Skip to main content

nemo_flow_ffi/api/
plugin.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::{
5    Arc, CStr, ConfigDiagnostic, DiagnosticLevel, FfiPluginContext, Future,
6    NemoFlowEventSubscriberCb, NemoFlowFreeFn, NemoFlowJsonCb, NemoFlowLlmConditionalCb,
7    NemoFlowLlmExecInterceptCb, NemoFlowLlmRequestCb, NemoFlowLlmRequestInterceptCb,
8    NemoFlowPluginRegisterCb, NemoFlowPluginValidateCb, NemoFlowStatus, NemoFlowToolConditionalCb,
9    NemoFlowToolExecInterceptCb, NemoFlowToolSanitizeCb, Pin, Plugin, PluginConfig, PluginError,
10    PluginRegistrationContext, active_plugin_report, c_char, c_str_to_json, c_str_to_string,
11    clear_last_error, clear_plugin_configuration, deregister_plugin, initialize_plugins,
12    json_to_c_string, last_error_message, list_plugin_kinds, nemo_flow_string_free,
13    register_adaptive_component, register_plugin, set_last_error, status_from_plugin_error,
14    tokio_runtime, validate_plugin_config, wrap_event_subscriber, wrap_llm_conditional_fn,
15    wrap_llm_exec_intercept_fn, wrap_llm_request_intercept_fn, wrap_llm_response_fn,
16    wrap_llm_sanitize_request_fn, wrap_llm_stream_exec_intercept_fn, wrap_tool_conditional_fn,
17    wrap_tool_exec_intercept_fn, wrap_tool_request_intercept_fn, wrap_tool_sanitize_fn,
18};
19
20struct FfiHostedPluginUserData {
21    ptr: *mut libc::c_void,
22    free_fn: NemoFlowFreeFn,
23}
24
25unsafe impl Send for FfiHostedPluginUserData {}
26unsafe impl Sync for FfiHostedPluginUserData {}
27
28impl Drop for FfiHostedPluginUserData {
29    fn drop(&mut self) {
30        if let Some(free_fn) = self.free_fn {
31            unsafe { free_fn(self.ptr) };
32        }
33    }
34}
35
36struct FfiHostedPluginAdapter {
37    plugin_kind: String,
38    validate_cb: Option<NemoFlowPluginValidateCb>,
39    register_cb: NemoFlowPluginRegisterCb,
40    user_data: Arc<FfiHostedPluginUserData>,
41}
42
43impl Plugin for FfiHostedPluginAdapter {
44    fn plugin_kind(&self) -> &str {
45        &self.plugin_kind
46    }
47
48    fn validate(
49        &self,
50        plugin_config: &serde_json::Map<String, serde_json::Value>,
51    ) -> Vec<ConfigDiagnostic> {
52        let Some(validate_cb) = self.validate_cb else {
53            return vec![];
54        };
55
56        clear_last_error();
57        let plugin_config_json =
58            json_to_c_string(&serde_json::Value::Object(plugin_config.clone()));
59        let result_ptr = unsafe { validate_cb(self.user_data.ptr, plugin_config_json) };
60        unsafe { nemo_flow_string_free(plugin_config_json) };
61
62        if result_ptr.is_null() {
63            let message = last_error_message().unwrap_or_else(|| {
64                format!(
65                    "plugin '{}' validate callback returned null",
66                    self.plugin_kind
67                )
68            });
69            return vec![ConfigDiagnostic {
70                level: DiagnosticLevel::Error,
71                code: "plugin.validate_failed".to_string(),
72                component: Some(self.plugin_kind.clone()),
73                field: None,
74                message,
75            }];
76        }
77
78        let diagnostics = unsafe { CStr::from_ptr(result_ptr) }
79            .to_str()
80            .ok()
81            .and_then(|text| serde_json::from_str::<Vec<ConfigDiagnostic>>(text).ok());
82        unsafe { nemo_flow_string_free(result_ptr) };
83        diagnostics.unwrap_or_else(|| {
84            vec![ConfigDiagnostic {
85                level: DiagnosticLevel::Error,
86                code: "plugin.validate_failed".to_string(),
87                component: Some(self.plugin_kind.clone()),
88                field: None,
89                message: format!(
90                    "plugin '{}' validate callback returned invalid diagnostics JSON",
91                    self.plugin_kind
92                ),
93            }]
94        })
95    }
96
97    fn register<'a>(
98        &'a self,
99        plugin_config: &serde_json::Map<String, serde_json::Value>,
100        ctx: &'a mut PluginRegistrationContext,
101    ) -> Pin<Box<dyn Future<Output = std::result::Result<(), PluginError>> + Send + 'a>> {
102        let plugin_config = plugin_config.clone();
103        Box::pin(async move {
104            clear_last_error();
105            let plugin_config_json = json_to_c_string(&serde_json::Value::Object(plugin_config));
106            let mut ffi_ctx = FfiPluginContext(ctx as *mut _);
107            let status =
108                unsafe { (self.register_cb)(self.user_data.ptr, plugin_config_json, &mut ffi_ctx) };
109            unsafe { nemo_flow_string_free(plugin_config_json) };
110            if status == NemoFlowStatus::Ok {
111                Ok(())
112            } else if let Some(message) = last_error_message() {
113                Err(PluginError::RegistrationFailed(message))
114            } else {
115                Err(PluginError::RegistrationFailed(format!(
116                    "plugin '{}' register callback failed with status {:?}",
117                    self.plugin_kind, status
118                )))
119            }
120        })
121    }
122}
123
124fn ensure_adaptive_component_registered() -> std::result::Result<(), NemoFlowStatus> {
125    register_adaptive_component().map_err(|err| status_from_plugin_error(&err))
126}
127
128/// Validate a generic plugin config document and return the diagnostics report as JSON.
129///
130/// # Safety
131/// `config_json` must be a valid C string and `out_json` must be a valid, non-null pointer.
132#[unsafe(no_mangle)]
133pub unsafe extern "C" fn nemo_flow_validate_plugin_config(
134    config_json: *const c_char,
135    out_json: *mut *mut c_char,
136) -> NemoFlowStatus {
137    clear_last_error();
138    if out_json.is_null() {
139        set_last_error("out_json pointer is null");
140        return NemoFlowStatus::NullPointer;
141    }
142    if let Err(status) = ensure_adaptive_component_registered() {
143        return status;
144    }
145    let config_value = match c_str_to_json(config_json) {
146        Some(value) => value,
147        None => return NemoFlowStatus::InvalidJson,
148    };
149    let config: PluginConfig = match serde_json::from_value(config_value) {
150        Ok(config) => config,
151        Err(err) => {
152            set_last_error(&err.to_string());
153            return NemoFlowStatus::InvalidJson;
154        }
155    };
156    let report_json = match serde_json::to_value(validate_plugin_config(&config)) {
157        Ok(value) => value,
158        Err(err) => {
159            set_last_error(&err.to_string());
160            return NemoFlowStatus::Internal;
161        }
162    };
163    unsafe { *out_json = json_to_c_string(&report_json) };
164    NemoFlowStatus::Ok
165}
166
167/// Initialize the active global plugin components and return the resulting diagnostics report.
168///
169/// # Safety
170/// `config_json` must be a valid C string and `out_json` must be a valid, non-null pointer.
171#[unsafe(no_mangle)]
172pub unsafe extern "C" fn nemo_flow_initialize_plugins(
173    config_json: *const c_char,
174    out_json: *mut *mut c_char,
175) -> NemoFlowStatus {
176    clear_last_error();
177    if out_json.is_null() {
178        set_last_error("out_json pointer is null");
179        return NemoFlowStatus::NullPointer;
180    }
181    if let Err(status) = ensure_adaptive_component_registered() {
182        return status;
183    }
184    let config_value = match c_str_to_json(config_json) {
185        Some(value) => value,
186        None => return NemoFlowStatus::InvalidJson,
187    };
188    let config: PluginConfig = match serde_json::from_value(config_value) {
189        Ok(config) => config,
190        Err(err) => {
191            set_last_error(&err.to_string());
192            return NemoFlowStatus::InvalidJson;
193        }
194    };
195    let report = match tokio_runtime().block_on(initialize_plugins(config)) {
196        Ok(report) => report,
197        Err(err) => return status_from_plugin_error(&err),
198    };
199    let report_json = match serde_json::to_value(report) {
200        Ok(value) => value,
201        Err(err) => {
202            set_last_error(&err.to_string());
203            return NemoFlowStatus::Internal;
204        }
205    };
206    unsafe { *out_json = json_to_c_string(&report_json) };
207    NemoFlowStatus::Ok
208}
209
210/// Clear the active global plugin configuration.
211#[unsafe(no_mangle)]
212pub extern "C" fn nemo_flow_clear_plugin_configuration() -> NemoFlowStatus {
213    clear_last_error();
214    match clear_plugin_configuration() {
215        Ok(()) => NemoFlowStatus::Ok,
216        Err(err) => status_from_plugin_error(&err),
217    }
218}
219
220/// Return the last successfully configured plugin report as JSON.
221///
222/// # Safety
223/// `out_json` must be a valid, non-null pointer.
224#[unsafe(no_mangle)]
225pub unsafe extern "C" fn nemo_flow_active_plugin_report_json(
226    out_json: *mut *mut c_char,
227) -> NemoFlowStatus {
228    clear_last_error();
229    if out_json.is_null() {
230        set_last_error("out_json pointer is null");
231        return NemoFlowStatus::NullPointer;
232    }
233    let report_json = match serde_json::to_value(active_plugin_report()) {
234        Ok(value) => value,
235        Err(err) => {
236            set_last_error(&err.to_string());
237            return NemoFlowStatus::Internal;
238        }
239    };
240    unsafe { *out_json = json_to_c_string(&report_json) };
241    NemoFlowStatus::Ok
242}
243
244/// Return the registered plugin kinds as JSON.
245///
246/// # Safety
247/// `out_json` must be a valid, non-null pointer.
248#[unsafe(no_mangle)]
249pub unsafe extern "C" fn nemo_flow_list_plugin_kinds_json(
250    out_json: *mut *mut c_char,
251) -> NemoFlowStatus {
252    clear_last_error();
253    if out_json.is_null() {
254        set_last_error("out_json pointer is null");
255        return NemoFlowStatus::NullPointer;
256    }
257    if let Err(status) = ensure_adaptive_component_registered() {
258        return status;
259    }
260    let kinds_json = match serde_json::to_value(list_plugin_kinds()) {
261        Ok(value) => value,
262        Err(err) => {
263            set_last_error(&err.to_string());
264            return NemoFlowStatus::Internal;
265        }
266    };
267    unsafe { *out_json = json_to_c_string(&kinds_json) };
268    NemoFlowStatus::Ok
269}
270
271/// Register a plugin backed by foreign callbacks.
272///
273/// # Safety
274/// `plugin_kind` must be a valid C string and `register_cb` must be a valid function pointer.
275#[unsafe(no_mangle)]
276pub unsafe extern "C" fn nemo_flow_register_plugin(
277    plugin_kind: *const c_char,
278    validate_cb: Option<NemoFlowPluginValidateCb>,
279    register_cb: NemoFlowPluginRegisterCb,
280    user_data: *mut libc::c_void,
281    free_fn: NemoFlowFreeFn,
282) -> NemoFlowStatus {
283    clear_last_error();
284    let plugin_kind = match c_str_to_string(plugin_kind) {
285        Ok(value) => value,
286        Err(status) => return status,
287    };
288
289    let plugin = Arc::new(FfiHostedPluginAdapter {
290        plugin_kind: plugin_kind.clone(),
291        validate_cb,
292        register_cb,
293        user_data: Arc::new(FfiHostedPluginUserData {
294            ptr: user_data,
295            free_fn,
296        }),
297    });
298    match register_plugin(plugin) {
299        Ok(()) => NemoFlowStatus::Ok,
300        Err(err) => status_from_plugin_error(&err),
301    }
302}
303
304/// Deregister a plugin by kind.
305///
306/// # Safety
307/// `plugin_kind` must be a valid C string.
308#[unsafe(no_mangle)]
309pub unsafe extern "C" fn nemo_flow_deregister_plugin(plugin_kind: *const c_char) -> NemoFlowStatus {
310    clear_last_error();
311    let plugin_kind = match c_str_to_string(plugin_kind) {
312        Ok(value) => value,
313        Err(status) => return status,
314    };
315    if deregister_plugin(&plugin_kind) {
316        NemoFlowStatus::Ok
317    } else {
318        set_last_error(&format!("not found: plugin '{plugin_kind}'"));
319        NemoFlowStatus::NotFound
320    }
321}
322
323/// Register an event subscriber into the plugin registration context.
324///
325/// # Safety
326/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
327/// of the plugin registration lifetime.
328#[unsafe(no_mangle)]
329pub unsafe extern "C" fn nemo_flow_plugin_context_register_subscriber(
330    ctx: *mut FfiPluginContext,
331    name: *const c_char,
332    cb: NemoFlowEventSubscriberCb,
333    user_data: *mut libc::c_void,
334    free_fn: NemoFlowFreeFn,
335) -> NemoFlowStatus {
336    clear_last_error();
337    if ctx.is_null() {
338        set_last_error("plugin context is null");
339        return NemoFlowStatus::NullPointer;
340    }
341    let name = match c_str_to_string(name) {
342        Ok(value) => value,
343        Err(status) => return status,
344    };
345    let wrapped = wrap_event_subscriber(cb, user_data, free_fn);
346    match unsafe { &mut *((*ctx).0) }.register_subscriber(&name, wrapped) {
347        Ok(()) => NemoFlowStatus::Ok,
348        Err(err) => status_from_plugin_error(&err),
349    }
350}
351
352/// Register a tool sanitize-request guardrail into the plugin registration context.
353///
354/// # Safety
355/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
356/// of the plugin registration lifetime.
357#[unsafe(no_mangle)]
358pub unsafe extern "C" fn nemo_flow_plugin_context_register_tool_sanitize_request_guardrail(
359    ctx: *mut FfiPluginContext,
360    name: *const c_char,
361    priority: i32,
362    cb: NemoFlowToolSanitizeCb,
363    user_data: *mut libc::c_void,
364    free_fn: NemoFlowFreeFn,
365) -> NemoFlowStatus {
366    clear_last_error();
367    if ctx.is_null() {
368        set_last_error("plugin context is null");
369        return NemoFlowStatus::NullPointer;
370    }
371    let name = match c_str_to_string(name) {
372        Ok(value) => value,
373        Err(status) => return status,
374    };
375    let wrapped = wrap_tool_sanitize_fn(cb, user_data, free_fn);
376    match unsafe { &mut *((*ctx).0) }
377        .register_tool_sanitize_request_guardrail(&name, priority, wrapped)
378    {
379        Ok(()) => NemoFlowStatus::Ok,
380        Err(err) => status_from_plugin_error(&err),
381    }
382}
383
384/// Register a tool sanitize-response guardrail into the plugin registration context.
385///
386/// # Safety
387/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
388/// of the plugin registration lifetime.
389#[unsafe(no_mangle)]
390pub unsafe extern "C" fn nemo_flow_plugin_context_register_tool_sanitize_response_guardrail(
391    ctx: *mut FfiPluginContext,
392    name: *const c_char,
393    priority: i32,
394    cb: NemoFlowToolSanitizeCb,
395    user_data: *mut libc::c_void,
396    free_fn: NemoFlowFreeFn,
397) -> NemoFlowStatus {
398    clear_last_error();
399    if ctx.is_null() {
400        set_last_error("plugin context is null");
401        return NemoFlowStatus::NullPointer;
402    }
403    let name = match c_str_to_string(name) {
404        Ok(value) => value,
405        Err(status) => return status,
406    };
407    let wrapped = wrap_tool_sanitize_fn(cb, user_data, free_fn);
408    match unsafe { &mut *((*ctx).0) }
409        .register_tool_sanitize_response_guardrail(&name, priority, wrapped)
410    {
411        Ok(()) => NemoFlowStatus::Ok,
412        Err(err) => status_from_plugin_error(&err),
413    }
414}
415
416/// Register a tool conditional-execution guardrail into the plugin registration context.
417///
418/// # Safety
419/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
420/// of the plugin registration lifetime.
421#[unsafe(no_mangle)]
422pub unsafe extern "C" fn nemo_flow_plugin_context_register_tool_conditional_execution_guardrail(
423    ctx: *mut FfiPluginContext,
424    name: *const c_char,
425    priority: i32,
426    cb: NemoFlowToolConditionalCb,
427    user_data: *mut libc::c_void,
428    free_fn: NemoFlowFreeFn,
429) -> NemoFlowStatus {
430    clear_last_error();
431    if ctx.is_null() {
432        set_last_error("plugin context is null");
433        return NemoFlowStatus::NullPointer;
434    }
435    let name = match c_str_to_string(name) {
436        Ok(value) => value,
437        Err(status) => return status,
438    };
439    let wrapped = wrap_tool_conditional_fn(cb, user_data, free_fn);
440    match unsafe { &mut *((*ctx).0) }
441        .register_tool_conditional_execution_guardrail(&name, priority, wrapped)
442    {
443        Ok(()) => NemoFlowStatus::Ok,
444        Err(err) => status_from_plugin_error(&err),
445    }
446}
447
448/// Register an LLM sanitize-request guardrail into the plugin registration context.
449///
450/// # Safety
451/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
452/// of the plugin registration lifetime.
453#[unsafe(no_mangle)]
454pub unsafe extern "C" fn nemo_flow_plugin_context_register_llm_sanitize_request_guardrail(
455    ctx: *mut FfiPluginContext,
456    name: *const c_char,
457    priority: i32,
458    cb: NemoFlowLlmRequestCb,
459    user_data: *mut libc::c_void,
460    free_fn: NemoFlowFreeFn,
461) -> NemoFlowStatus {
462    clear_last_error();
463    if ctx.is_null() {
464        set_last_error("plugin context is null");
465        return NemoFlowStatus::NullPointer;
466    }
467    let name = match c_str_to_string(name) {
468        Ok(value) => value,
469        Err(status) => return status,
470    };
471    let wrapped = wrap_llm_sanitize_request_fn(cb, user_data, free_fn);
472    match unsafe { &mut *((*ctx).0) }
473        .register_llm_sanitize_request_guardrail(&name, priority, wrapped)
474    {
475        Ok(()) => NemoFlowStatus::Ok,
476        Err(err) => status_from_plugin_error(&err),
477    }
478}
479
480/// Register an LLM sanitize-response guardrail into the plugin registration context.
481///
482/// # Safety
483/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
484/// of the plugin registration lifetime.
485#[unsafe(no_mangle)]
486pub unsafe extern "C" fn nemo_flow_plugin_context_register_llm_sanitize_response_guardrail(
487    ctx: *mut FfiPluginContext,
488    name: *const c_char,
489    priority: i32,
490    cb: NemoFlowJsonCb,
491    user_data: *mut libc::c_void,
492    free_fn: NemoFlowFreeFn,
493) -> NemoFlowStatus {
494    clear_last_error();
495    if ctx.is_null() {
496        set_last_error("plugin context is null");
497        return NemoFlowStatus::NullPointer;
498    }
499    let name = match c_str_to_string(name) {
500        Ok(value) => value,
501        Err(status) => return status,
502    };
503    let wrapped = wrap_llm_response_fn(cb, user_data, free_fn);
504    match unsafe { &mut *((*ctx).0) }
505        .register_llm_sanitize_response_guardrail(&name, priority, wrapped)
506    {
507        Ok(()) => NemoFlowStatus::Ok,
508        Err(err) => status_from_plugin_error(&err),
509    }
510}
511
512/// Register an LLM conditional-execution guardrail into the plugin registration context.
513///
514/// # Safety
515/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
516/// of the plugin registration lifetime.
517#[unsafe(no_mangle)]
518pub unsafe extern "C" fn nemo_flow_plugin_context_register_llm_conditional_execution_guardrail(
519    ctx: *mut FfiPluginContext,
520    name: *const c_char,
521    priority: i32,
522    cb: NemoFlowLlmConditionalCb,
523    user_data: *mut libc::c_void,
524    free_fn: NemoFlowFreeFn,
525) -> NemoFlowStatus {
526    clear_last_error();
527    if ctx.is_null() {
528        set_last_error("plugin context is null");
529        return NemoFlowStatus::NullPointer;
530    }
531    let name = match c_str_to_string(name) {
532        Ok(value) => value,
533        Err(status) => return status,
534    };
535    let wrapped = wrap_llm_conditional_fn(cb, user_data, free_fn);
536    match unsafe { &mut *((*ctx).0) }
537        .register_llm_conditional_execution_guardrail(&name, priority, wrapped)
538    {
539        Ok(()) => NemoFlowStatus::Ok,
540        Err(err) => status_from_plugin_error(&err),
541    }
542}
543
544/// Register an LLM request intercept into the plugin registration context.
545///
546/// # Safety
547/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
548/// of the plugin registration lifetime.
549#[unsafe(no_mangle)]
550pub unsafe extern "C" fn nemo_flow_plugin_context_register_llm_request_intercept(
551    ctx: *mut FfiPluginContext,
552    name: *const c_char,
553    priority: i32,
554    break_chain: bool,
555    cb: NemoFlowLlmRequestInterceptCb,
556    user_data: *mut libc::c_void,
557    free_fn: NemoFlowFreeFn,
558) -> NemoFlowStatus {
559    clear_last_error();
560    if ctx.is_null() {
561        set_last_error("plugin context is null");
562        return NemoFlowStatus::NullPointer;
563    }
564    let name = match c_str_to_string(name) {
565        Ok(value) => value,
566        Err(status) => return status,
567    };
568    let wrapped = wrap_llm_request_intercept_fn(cb, user_data, free_fn);
569    match unsafe { &mut *((*ctx).0) }.register_llm_request_intercept(
570        &name,
571        priority,
572        break_chain,
573        wrapped,
574    ) {
575        Ok(()) => NemoFlowStatus::Ok,
576        Err(err) => status_from_plugin_error(&err),
577    }
578}
579
580/// Register a tool request intercept into the plugin registration context.
581///
582/// # Safety
583/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
584/// of the plugin registration lifetime.
585#[unsafe(no_mangle)]
586pub unsafe extern "C" fn nemo_flow_plugin_context_register_tool_request_intercept(
587    ctx: *mut FfiPluginContext,
588    name: *const c_char,
589    priority: i32,
590    break_chain: bool,
591    cb: NemoFlowToolSanitizeCb,
592    user_data: *mut libc::c_void,
593    free_fn: NemoFlowFreeFn,
594) -> NemoFlowStatus {
595    clear_last_error();
596    if ctx.is_null() {
597        set_last_error("plugin context is null");
598        return NemoFlowStatus::NullPointer;
599    }
600    let name = match c_str_to_string(name) {
601        Ok(value) => value,
602        Err(status) => return status,
603    };
604    let wrapped = wrap_tool_request_intercept_fn(cb, user_data, free_fn);
605    match unsafe { &mut *((*ctx).0) }.register_tool_request_intercept(
606        &name,
607        priority,
608        break_chain,
609        wrapped,
610    ) {
611        Ok(()) => NemoFlowStatus::Ok,
612        Err(err) => status_from_plugin_error(&err),
613    }
614}
615
616/// Register an LLM execution intercept into the plugin registration context.
617///
618/// # Safety
619/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
620/// of the plugin registration lifetime.
621#[unsafe(no_mangle)]
622pub unsafe extern "C" fn nemo_flow_plugin_context_register_llm_execution_intercept(
623    ctx: *mut FfiPluginContext,
624    name: *const c_char,
625    priority: i32,
626    cb: NemoFlowLlmExecInterceptCb,
627    user_data: *mut libc::c_void,
628    free_fn: NemoFlowFreeFn,
629) -> NemoFlowStatus {
630    clear_last_error();
631    if ctx.is_null() {
632        set_last_error("plugin context is null");
633        return NemoFlowStatus::NullPointer;
634    }
635    let name = match c_str_to_string(name) {
636        Ok(value) => value,
637        Err(status) => return status,
638    };
639    let wrapped = wrap_llm_exec_intercept_fn(cb, user_data, free_fn);
640    match unsafe { &mut *((*ctx).0) }.register_llm_execution_intercept(&name, priority, wrapped) {
641        Ok(()) => NemoFlowStatus::Ok,
642        Err(err) => status_from_plugin_error(&err),
643    }
644}
645
646/// Register an LLM stream execution intercept into the plugin registration context.
647///
648/// # Safety
649/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
650/// of the plugin registration lifetime.
651#[unsafe(no_mangle)]
652pub unsafe extern "C" fn nemo_flow_plugin_context_register_llm_stream_execution_intercept(
653    ctx: *mut FfiPluginContext,
654    name: *const c_char,
655    priority: i32,
656    cb: NemoFlowLlmExecInterceptCb,
657    user_data: *mut libc::c_void,
658    free_fn: NemoFlowFreeFn,
659) -> NemoFlowStatus {
660    clear_last_error();
661    if ctx.is_null() {
662        set_last_error("plugin context is null");
663        return NemoFlowStatus::NullPointer;
664    }
665    let name = match c_str_to_string(name) {
666        Ok(value) => value,
667        Err(status) => return status,
668    };
669    let wrapped = wrap_llm_stream_exec_intercept_fn(cb, user_data, free_fn);
670    match unsafe { &mut *((*ctx).0) }
671        .register_llm_stream_execution_intercept(&name, priority, wrapped)
672    {
673        Ok(()) => NemoFlowStatus::Ok,
674        Err(err) => status_from_plugin_error(&err),
675    }
676}
677
678/// Register a tool execution intercept into the plugin registration context.
679///
680/// # Safety
681/// `ctx` and `name` must be valid pointers and the callback must remain valid for the duration
682/// of the plugin registration lifetime.
683#[unsafe(no_mangle)]
684pub unsafe extern "C" fn nemo_flow_plugin_context_register_tool_execution_intercept(
685    ctx: *mut FfiPluginContext,
686    name: *const c_char,
687    priority: i32,
688    cb: NemoFlowToolExecInterceptCb,
689    user_data: *mut libc::c_void,
690    free_fn: NemoFlowFreeFn,
691) -> NemoFlowStatus {
692    clear_last_error();
693    if ctx.is_null() {
694        set_last_error("plugin context is null");
695        return NemoFlowStatus::NullPointer;
696    }
697    let name = match c_str_to_string(name) {
698        Ok(value) => value,
699        Err(status) => return status,
700    };
701    let wrapped = wrap_tool_exec_intercept_fn(cb, user_data, free_fn);
702    match unsafe { &mut *((*ctx).0) }.register_tool_execution_intercept(&name, priority, wrapped) {
703        Ok(()) => NemoFlowStatus::Ok,
704        Err(err) => status_from_plugin_error(&err),
705    }
706}