cloacina_workflow_plugin/lib.rs
1/*
2 * Copyright 2025-2026 Colliery Software
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Cloacina plugin interface for the fidius plugin system.
18//!
19//! This crate defines the plugin contract between cloacina workflow packages
20//! (compiled as cdylib plugins) and the cloacina host engine. Both sides
21//! depend on this crate — it is the single source of truth for the FFI ABI.
22//!
23//! # For plugin authors
24//!
25//! Use `#[workflow]` with `features = ["packaged"]` — the macro generates
26//! the `#[plugin_impl(CloacinaPlugin)]` code automatically. You don't need
27//! to implement this trait directly.
28//!
29//! # For host integration
30//!
31//! Use `fidius-host` to load plugins and call methods through `PluginHandle`.
32//! Validate loaded plugins against `CloacinaPlugin_INTERFACE_HASH` to detect
33//! ABI drift at load time.
34
35// The `#[fidius::plugin_interface]` macro on `CloacinaPlugin` emits a
36// `#[cfg(host)]` gate and produces methods whose arity exceeds clippy's
37// default 7-argument cap. Both warnings are internal to the fidius
38// macro and not actionable at our call site.
39#![allow(unexpected_cfgs, clippy::too_many_arguments)]
40
41pub mod inventory_entries;
42pub mod types;
43
44pub use inventory_entries::{
45 ComputationGraphEntry, ReactorEntry, TaskEntry, TriggerEntry, TriggerlessGraph,
46 TriggerlessGraphEntry, TriggerlessGraphFn, TriggerlessGraphRegistration,
47 WorkflowDescriptorEntry,
48};
49
50// Re-export the interface types for convenience
51pub use types::{
52 AccumulatorDeclarationEntry, CloacinaMetadata, GraphExecutionRequest, GraphExecutionResult,
53 GraphPackageMetadata, PackageTasksMetadata, ReactorPackageMetadata, TaskExecutionRequest,
54 TaskExecutionResult, TaskMetadataEntry, TriggerInvokeRequest, TriggerInvokeResult,
55 TriggerPackageMetadata, TriggerlessGraphInvokeRequest, TriggerlessGraphInvokeResult,
56 TriggerlessGraphMetadataEntry,
57};
58
59// Re-export fidius crates so generated code can reference them
60pub use fidius;
61pub use fidius_core;
62
63// Re-export fidius modules needed by generated code when crate = "cloacina_workflow_plugin"
64pub use fidius_core::descriptor;
65pub use fidius_core::inventory;
66pub use fidius_core::registry;
67pub use fidius_core::status;
68pub use fidius_core::wire;
69
70// Re-export fidius types that plugin authors need
71pub use fidius::plugin_impl;
72pub use fidius_core::error::PluginError;
73pub use fidius_core::package::{PackageHeader, PackageManifest};
74
75// Re-export the plugin registry macro
76pub use fidius::fidius_plugin_registry;
77
78/// Unified plugin shell macro for I-0102.
79///
80/// Single-line invocation at the crate root of any packaged Rust cdylib:
81///
82/// ```rust,ignore
83/// cloacina::package!();
84/// ```
85///
86/// Emits, gated on `#[cfg(feature = "packaged")]`:
87///
88/// 1. A `pub mod _ffi` containing a `CloacinaPackagePlugin` unit struct.
89/// 2. A `#[plugin_impl]` block exporting the version-2 `CloacinaPlugin`
90/// trait. Six methods: `get_task_metadata`, `execute_task`,
91/// `get_graph_metadata`, `execute_graph`, `get_reactor_metadata`,
92/// `get_trigger_metadata`. The reactor body walks
93/// `inventory::iter::<ReactorEntry>` and projects each entry to a
94/// `ReactorPackageMetadata` value. The other bodies are stubs at this
95/// iteration of T-A — they ship empty/NotImplemented and will be
96/// fleshed out as the remaining inventory entry types relocate to leaf
97/// crates reachable from packaged cdylibs.
98/// 3. A `mod __cloacina_package_marker { pub struct Once; }` guard. A
99/// second invocation in the same crate fails to compile (duplicate
100/// module name).
101/// 4. A `fidius_plugin_registry!()` invocation to export the plugin.
102///
103/// **Coexistence:** in T-A the per-macro `_ffi` emission from
104/// `#[computation_graph]` and `#[workflow]` is unchanged. A crate that
105/// adds `cloacina::package!();` AND has `#[computation_graph]` /
106/// `#[workflow]` would emit two `fidius_plugin_registry!()` calls →
107/// linker conflict. T-C strips the per-macro emission so the shell
108/// becomes the only path.
109#[macro_export]
110macro_rules! package {
111 () => {
112 #[cfg(feature = "packaged")]
113 pub mod _ffi {
114 use $crate::CloacinaPlugin as _;
115 use $crate::__fidius_CloacinaPlugin;
116
117 // Single-emission guard: a duplicate `cloacina::package!()` in
118 // the same crate produces "the name `__cloacina_package_marker`
119 // is defined multiple times" at compile time.
120 mod __cloacina_package_marker {
121 pub struct Once;
122 }
123
124 pub struct CloacinaPackagePlugin;
125
126 #[$crate::plugin_impl(CloacinaPlugin, crate = "cloacina_workflow_plugin")]
127 impl $crate::CloacinaPlugin for CloacinaPackagePlugin {
128 fn get_task_metadata(
129 &self,
130 ) -> ::core::result::Result<$crate::PackageTasksMetadata, $crate::PluginError>
131 {
132 let mut tasks: ::std::vec::Vec<$crate::TaskMetadataEntry> =
133 ::std::vec::Vec::new();
134 let mut workflow_name: ::std::string::String = ::std::string::String::new();
135 for (idx, entry) in $crate::inventory::iter::<$crate::TaskEntry>
136 .into_iter()
137 .enumerate()
138 {
139 let ns = (entry.namespace)();
140 if workflow_name.is_empty() {
141 workflow_name = ns.workflow_id.clone();
142 }
143 let task = (entry.constructor)();
144 let dependencies: ::std::vec::Vec<::std::string::String> =
145 cloacina_workflow::Task::dependencies(&*task)
146 .iter()
147 .map(|n| n.task_id.clone())
148 .collect();
149 tasks.push($crate::TaskMetadataEntry {
150 index: idx as u32,
151 id: cloacina_workflow::Task::id(&*task).to_string(),
152 namespaced_id_template: format!(
153 "{}::{}::{}::{}",
154 ns.tenant_id, ns.package_name, ns.workflow_id, ns.task_id,
155 ),
156 dependencies,
157 description: format!("Task: {}", cloacina_workflow::Task::id(&*task)),
158 source_location: format!("{}/lib.rs", env!("CARGO_PKG_NAME")),
159 });
160 }
161 // Look up WorkflowDescriptorEntry for description / author /
162 // fingerprint / graph_data / triggers. Multiple workflows in
163 // the same cdylib aren't supported by the shell — the first
164 // descriptor wins; T-D fixtures verify single-workflow
165 // assumption.
166 let descriptor = $crate::inventory::iter::<$crate::WorkflowDescriptorEntry>
167 .into_iter()
168 .next();
169 let (description, author, fingerprint, graph_data_json, triggers) =
170 match descriptor {
171 Some(d) => (
172 if d.description.is_empty() {
173 None
174 } else {
175 Some(d.description.to_string())
176 },
177 if d.author.is_empty() {
178 None
179 } else {
180 Some(d.author.to_string())
181 },
182 if d.fingerprint.is_empty() {
183 None
184 } else {
185 Some(d.fingerprint.to_string())
186 },
187 if d.graph_data_json.is_empty() {
188 None
189 } else {
190 Some(d.graph_data_json.to_string())
191 },
192 (d.triggers)(),
193 ),
194 None => (None, None, None, None, ::std::vec::Vec::new()),
195 };
196 Ok($crate::PackageTasksMetadata {
197 workflow_name,
198 package_name: env!("CARGO_PKG_NAME").to_string(),
199 package_description: description,
200 package_author: author,
201 workflow_fingerprint: fingerprint,
202 graph_data_json,
203 tasks,
204 triggers,
205 })
206 }
207
208 fn execute_task(
209 &self,
210 request: $crate::TaskExecutionRequest,
211 ) -> ::core::result::Result<$crate::TaskExecutionResult, $crate::PluginError>
212 {
213 use $crate::CloacinaPlugin as _;
214 static CDYLIB_RUNTIME: ::std::sync::OnceLock<
215 cloacina_workflow::__private::tokio::runtime::Runtime,
216 > = ::std::sync::OnceLock::new();
217 let rt = CDYLIB_RUNTIME.get_or_init(|| {
218 cloacina_workflow::__private::tokio::runtime::Builder::new_multi_thread()
219 .enable_all()
220 .worker_threads(2)
221 .thread_name("package-shell-cdylib-worker")
222 .build()
223 .expect("Failed to create cdylib tokio runtime")
224 });
225
226 // Resolve the named task by walking inventory.
227 let task_arc_opt = $crate::inventory::iter::<$crate::TaskEntry>
228 .into_iter()
229 .map(|entry| (entry.constructor)())
230 .find(|t| cloacina_workflow::Task::id(&**t) == request.task_name);
231
232 let task = match task_arc_opt {
233 Some(t) => t,
234 None => {
235 return Ok($crate::TaskExecutionResult {
236 success: false,
237 context_json: None,
238 error: Some(format!("Unknown task: {}", request.task_name)),
239 });
240 }
241 };
242
243 let context: cloacina_workflow::Context<::serde_json::Value> =
244 match cloacina_workflow::Context::from_json(request.context_json) {
245 Ok(c) => c,
246 Err(e) => {
247 return Err($crate::PluginError {
248 code: "CONTEXT_ERROR".to_string(),
249 message: format!("Failed to parse context: {}", e),
250 details: None,
251 });
252 }
253 };
254
255 let result = rt.block_on(async move {
256 cloacina_workflow::Task::execute(&*task, context).await
257 });
258
259 match result {
260 Ok(updated) => {
261 let ctx_json = updated.to_json().map_err(|e| $crate::PluginError {
262 code: "SERIALIZATION_ERROR".to_string(),
263 message: format!("Failed to serialize context: {}", e),
264 details: None,
265 })?;
266 Ok($crate::TaskExecutionResult {
267 success: true,
268 context_json: Some(ctx_json),
269 error: None,
270 })
271 }
272 Err(e) => Ok($crate::TaskExecutionResult {
273 success: false,
274 context_json: None,
275 error: Some(format!("Task '{}' failed: {:?}", request.task_name, e)),
276 }),
277 }
278 }
279
280 fn get_graph_metadata(
281 &self,
282 ) -> ::core::result::Result<$crate::GraphPackageMetadata, $crate::PluginError>
283 {
284 let entries: ::std::vec::Vec<&$crate::ComputationGraphEntry> =
285 $crate::inventory::iter::<$crate::ComputationGraphEntry>
286 .into_iter()
287 .collect();
288 if entries.is_empty() {
289 return Err($crate::PluginError {
290 code: "NOT_SUPPORTED".to_string(),
291 message: "Package declares no computation graph".to_string(),
292 details: None,
293 });
294 }
295 if entries.len() > 1 {
296 return Err($crate::PluginError {
297 code: "MULTIPLE_GRAPHS".to_string(),
298 message: format!(
299 "Package declares {} computation graphs; the unified shell \
300 supports at most one CG per cdylib",
301 entries.len()
302 ),
303 details: None,
304 });
305 }
306 let reg = (entries[0].constructor)();
307 let accumulators: ::std::vec::Vec<$crate::AccumulatorDeclarationEntry> = reg
308 .accumulator_names
309 .iter()
310 .map(|name| $crate::AccumulatorDeclarationEntry {
311 name: name.clone(),
312 accumulator_type: "passthrough".to_string(),
313 config: ::std::collections::HashMap::new(),
314 })
315 .collect();
316 Ok($crate::GraphPackageMetadata {
317 graph_name: entries[0].name.to_string(),
318 package_name: env!("CARGO_PKG_NAME").to_string(),
319 reaction_mode: reg.reaction_mode.clone(),
320 input_strategy: "latest".to_string(),
321 accumulators,
322 trigger_reactor: reg.trigger_reactor.clone(),
323 })
324 }
325
326 fn execute_graph(
327 &self,
328 request: $crate::GraphExecutionRequest,
329 ) -> ::core::result::Result<$crate::GraphExecutionResult, $crate::PluginError>
330 {
331 static CDYLIB_RUNTIME: ::std::sync::OnceLock<
332 cloacina_workflow::__private::tokio::runtime::Runtime,
333 > = ::std::sync::OnceLock::new();
334 let rt = CDYLIB_RUNTIME.get_or_init(|| {
335 cloacina_workflow::__private::tokio::runtime::Builder::new_multi_thread()
336 .enable_all()
337 .worker_threads(2)
338 .thread_name("package-shell-cg-worker")
339 .build()
340 .expect("Failed to create cdylib tokio runtime for computation graph")
341 });
342
343 let entry_opt = $crate::inventory::iter::<$crate::ComputationGraphEntry>
344 .into_iter()
345 .next();
346 let entry = match entry_opt {
347 Some(e) => e,
348 None => {
349 return Err($crate::PluginError {
350 code: "NOT_SUPPORTED".to_string(),
351 message: "Package declares no computation graph".to_string(),
352 details: None,
353 });
354 }
355 };
356 let reg = (entry.constructor)();
357
358 let mut cache = cloacina_computation_graph::InputCache::new();
359 for (source_name, json_str) in &request.cache {
360 let value: ::serde_json::Value =
361 ::serde_json::from_str(json_str).map_err(|e| $crate::PluginError {
362 code: "DESERIALIZATION_ERROR".to_string(),
363 message: format!(
364 "Failed to parse cache entry '{}': {}",
365 source_name, e
366 ),
367 details: None,
368 })?;
369 let bytes = cloacina_computation_graph::serialize(&value).map_err(|e| {
370 $crate::PluginError {
371 code: "SERIALIZATION_ERROR".to_string(),
372 message: format!(
373 "Failed to serialize cache entry '{}': {}",
374 source_name, e
375 ),
376 details: None,
377 }
378 })?;
379 cache.update(
380 cloacina_computation_graph::SourceName::new(source_name),
381 bytes,
382 );
383 }
384
385 let result = rt.block_on(async { (reg.graph_fn)(cache).await });
386
387 match result {
388 cloacina_computation_graph::GraphResult::Completed { outputs } => {
389 let terminal_json: ::std::vec::Vec<::std::string::String> = outputs
390 .iter()
391 .filter_map(|o| {
392 o.downcast_ref::<::serde_json::Value>()
393 .map(|v| ::serde_json::to_string(v).unwrap_or_default())
394 })
395 .collect();
396 Ok($crate::GraphExecutionResult {
397 success: true,
398 terminal_outputs_json: if terminal_json.is_empty() {
399 None
400 } else {
401 Some(terminal_json)
402 },
403 error: None,
404 })
405 }
406 cloacina_computation_graph::GraphResult::Error(e) => {
407 Ok($crate::GraphExecutionResult {
408 success: false,
409 terminal_outputs_json: None,
410 error: Some(format!("{}", e)),
411 })
412 }
413 }
414 }
415
416 fn get_reactor_metadata(
417 &self,
418 ) -> ::core::result::Result<
419 ::std::vec::Vec<$crate::ReactorPackageMetadata>,
420 $crate::PluginError,
421 > {
422 let mut out: ::std::vec::Vec<$crate::ReactorPackageMetadata> =
423 ::std::vec::Vec::new();
424 for entry in $crate::inventory::iter::<$crate::ReactorEntry> {
425 let reg = (entry.constructor)();
426 let accumulators: ::std::vec::Vec<$crate::AccumulatorDeclarationEntry> =
427 reg.accumulator_names
428 .iter()
429 .map(|name| $crate::AccumulatorDeclarationEntry {
430 name: name.clone(),
431 accumulator_type: "passthrough".to_string(),
432 config: ::std::collections::HashMap::new(),
433 })
434 .collect();
435 out.push($crate::ReactorPackageMetadata {
436 name: reg.name,
437 package_name: env!("CARGO_PKG_NAME").to_string(),
438 reaction_mode: reg.reaction_mode.as_str().to_string(),
439 accumulators,
440 });
441 }
442 Ok(out)
443 }
444
445 fn get_trigger_metadata(
446 &self,
447 ) -> ::core::result::Result<
448 ::std::vec::Vec<$crate::TriggerPackageMetadata>,
449 $crate::PluginError,
450 > {
451 let mut out: ::std::vec::Vec<$crate::TriggerPackageMetadata> =
452 ::std::vec::Vec::new();
453 for entry in $crate::inventory::iter::<$crate::TriggerEntry> {
454 let trigger = (entry.constructor)();
455 let poll_interval = format!(
456 "{}ms",
457 cloacina_workflow::Trigger::poll_interval(&*trigger).as_millis()
458 );
459 out.push($crate::TriggerPackageMetadata {
460 name: entry.name.to_string(),
461 package_name: env!("CARGO_PKG_NAME").to_string(),
462 poll_interval,
463 cron_expression: cloacina_workflow::Trigger::cron_expression(&*trigger),
464 allow_concurrent: cloacina_workflow::Trigger::allow_concurrent(
465 &*trigger,
466 ),
467 });
468 }
469 Ok(out)
470 }
471
472 fn invoke_trigger_poll(
473 &self,
474 request: $crate::TriggerInvokeRequest,
475 ) -> ::core::result::Result<$crate::TriggerInvokeResult, $crate::PluginError>
476 {
477 static CDYLIB_TRIGGER_RUNTIME: ::std::sync::OnceLock<
478 cloacina_workflow::__private::tokio::runtime::Runtime,
479 > = ::std::sync::OnceLock::new();
480 let rt = CDYLIB_TRIGGER_RUNTIME.get_or_init(|| {
481 cloacina_workflow::__private::tokio::runtime::Builder::new_multi_thread()
482 .enable_all()
483 .worker_threads(2)
484 .thread_name("package-shell-trigger-worker")
485 .build()
486 .expect("Failed to create cdylib trigger tokio runtime")
487 });
488
489 let trigger_arc_opt = $crate::inventory::iter::<$crate::TriggerEntry>
490 .into_iter()
491 .find(|entry| entry.name == request.trigger_name)
492 .map(|entry| (entry.constructor)());
493
494 let trigger = match trigger_arc_opt {
495 Some(t) => t,
496 None => {
497 return Ok($crate::TriggerInvokeResult {
498 fire: false,
499 context_json: None,
500 error: Some(format!("Unknown trigger: {}", request.trigger_name)),
501 });
502 }
503 };
504
505 let poll_result = rt
506 .block_on(async move { cloacina_workflow::Trigger::poll(&*trigger).await });
507
508 match poll_result {
509 Ok(cloacina_workflow::TriggerResult::Skip) => {
510 Ok($crate::TriggerInvokeResult {
511 fire: false,
512 context_json: None,
513 error: None,
514 })
515 }
516 Ok(cloacina_workflow::TriggerResult::Fire(None)) => {
517 Ok($crate::TriggerInvokeResult {
518 fire: true,
519 context_json: None,
520 error: None,
521 })
522 }
523 Ok(cloacina_workflow::TriggerResult::Fire(Some(ctx))) => {
524 match ctx.to_json() {
525 Ok(ctx_json) => Ok($crate::TriggerInvokeResult {
526 fire: true,
527 context_json: Some(ctx_json),
528 error: None,
529 }),
530 Err(e) => Err($crate::PluginError {
531 code: "SERIALIZATION_ERROR".to_string(),
532 message: format!("Failed to serialize trigger context: {}", e),
533 details: None,
534 }),
535 }
536 }
537 Err(e) => Ok($crate::TriggerInvokeResult {
538 fire: false,
539 context_json: None,
540 error: Some(format!(
541 "Trigger '{}' poll failed: {:?}",
542 request.trigger_name, e
543 )),
544 }),
545 }
546 }
547
548 fn get_triggerless_graph_metadata(
549 &self,
550 ) -> ::core::result::Result<
551 ::std::vec::Vec<$crate::TriggerlessGraphMetadataEntry>,
552 $crate::PluginError,
553 > {
554 let mut out: ::std::vec::Vec<$crate::TriggerlessGraphMetadataEntry> =
555 ::std::vec::Vec::new();
556 for entry in $crate::inventory::iter::<$crate::TriggerlessGraphEntry> {
557 let reg = (entry.constructor)();
558 out.push($crate::TriggerlessGraphMetadataEntry {
559 name: entry.name.to_string(),
560 package_name: env!("CARGO_PKG_NAME").to_string(),
561 terminal_node_names: reg.terminal_node_names.clone(),
562 });
563 }
564 Ok(out)
565 }
566
567 fn invoke_triggerless_graph(
568 &self,
569 request: $crate::TriggerlessGraphInvokeRequest,
570 ) -> ::core::result::Result<
571 $crate::TriggerlessGraphInvokeResult,
572 $crate::PluginError,
573 > {
574 static CDYLIB_TLCG_RUNTIME: ::std::sync::OnceLock<
575 cloacina_workflow::__private::tokio::runtime::Runtime,
576 > = ::std::sync::OnceLock::new();
577 let rt = CDYLIB_TLCG_RUNTIME.get_or_init(|| {
578 cloacina_workflow::__private::tokio::runtime::Builder::new_multi_thread()
579 .enable_all()
580 .worker_threads(2)
581 .thread_name("package-shell-tlcg-worker")
582 .build()
583 .expect("Failed to create cdylib trigger-less CG tokio runtime")
584 });
585
586 let entry_opt = $crate::inventory::iter::<$crate::TriggerlessGraphEntry>
587 .into_iter()
588 .find(|e| e.name == request.graph_name);
589 let entry = match entry_opt {
590 Some(e) => e,
591 None => {
592 return Ok($crate::TriggerlessGraphInvokeResult {
593 success: false,
594 terminal_outputs_json: None,
595 error: Some(format!(
596 "Unknown trigger-less graph: {}",
597 request.graph_name
598 )),
599 });
600 }
601 };
602
603 let context: cloacina_workflow::Context<::serde_json::Value> =
604 match cloacina_workflow::Context::from_json(request.context_json) {
605 Ok(c) => c,
606 Err(e) => {
607 return Err($crate::PluginError {
608 code: "CONTEXT_ERROR".to_string(),
609 message: format!(
610 "Failed to parse context for graph '{}': {}",
611 request.graph_name, e
612 ),
613 details: None,
614 });
615 }
616 };
617
618 let reg = (entry.constructor)();
619 let graph_fn = reg.graph_fn.clone();
620 let terminal_names = reg.terminal_node_names.clone();
621 let result = rt.block_on(async move { graph_fn(context).await });
622
623 match result {
624 ::cloacina_computation_graph::GraphResult::Completed { outputs } => {
625 let mut json_outputs: ::std::vec::Vec<::serde_json::Value> =
626 ::std::vec::Vec::with_capacity(outputs.len());
627 for boxed in outputs.iter() {
628 if let Some(value) = boxed.downcast_ref::<::serde_json::Value>() {
629 json_outputs.push(value.clone());
630 } else {
631 json_outputs.push(::serde_json::Value::Null);
632 }
633 }
634 while json_outputs.len() < terminal_names.len() {
635 json_outputs.push(::serde_json::Value::Null);
636 }
637 let outputs_json = match ::serde_json::to_string(&json_outputs) {
638 Ok(s) => s,
639 Err(e) => {
640 return Err($crate::PluginError {
641 code: "SERIALIZATION_ERROR".to_string(),
642 message: format!(
643 "Failed to serialize terminal outputs: {}",
644 e
645 ),
646 details: None,
647 });
648 }
649 };
650 Ok($crate::TriggerlessGraphInvokeResult {
651 success: true,
652 terminal_outputs_json: Some(outputs_json),
653 error: None,
654 })
655 }
656 ::cloacina_computation_graph::GraphResult::Error(err) => {
657 Ok($crate::TriggerlessGraphInvokeResult {
658 success: false,
659 terminal_outputs_json: None,
660 error: Some(format!(
661 "Graph '{}' failed: {}",
662 request.graph_name, err
663 )),
664 })
665 }
666 }
667 }
668 }
669
670 $crate::fidius_plugin_registry!();
671 }
672 };
673}
674
675/// Method index constants for the `CloacinaPlugin` trait. The fidius
676/// plugin ABI dispatches by positional method index — call sites on the
677/// host (and any out-of-tree consumers) must match the order in which
678/// methods are declared on the trait below. Defining them here in the
679/// canonical interface crate keeps host and plugin in lockstep: a
680/// reorder of trait methods is impossible without also re-numbering
681/// these constants.
682pub const METHOD_GET_TASK_METADATA: usize = 0;
683/// See [`METHOD_GET_TASK_METADATA`].
684pub const METHOD_EXECUTE_TASK: usize = 1;
685/// See [`METHOD_GET_TASK_METADATA`].
686pub const METHOD_GET_GRAPH_METADATA: usize = 2;
687/// See [`METHOD_GET_TASK_METADATA`].
688pub const METHOD_EXECUTE_GRAPH: usize = 3;
689/// See [`METHOD_GET_TASK_METADATA`].
690pub const METHOD_GET_REACTOR_METADATA: usize = 4;
691/// See [`METHOD_GET_TASK_METADATA`].
692pub const METHOD_GET_TRIGGER_METADATA: usize = 5;
693/// See [`METHOD_GET_TASK_METADATA`].
694pub const METHOD_INVOKE_TRIGGER_POLL: usize = 6;
695/// See [`METHOD_GET_TASK_METADATA`].
696pub const METHOD_GET_TRIGGERLESS_GRAPH_METADATA: usize = 7;
697/// See [`METHOD_GET_TASK_METADATA`].
698pub const METHOD_INVOKE_TRIGGERLESS_GRAPH: usize = 8;
699
700/// The plugin interface for cloacina workflow packages.
701///
702/// Every packaged workflow implements this trait (via `#[plugin_impl]` generated
703/// by the `#[workflow]` macro). The host calls these methods through a fidius
704/// `PluginHandle` — never directly.
705///
706/// ## Methods
707///
708/// - `get_task_metadata` — Returns metadata about all tasks in the workflow
709/// (IDs, dependencies, descriptions). Called once at registration time.
710/// - `execute_task` — Runs a specific task by name with a JSON-serialized
711/// context. Returns the updated context or an error.
712#[fidius::plugin_interface(version = 2, buffer = PluginAllocated)]
713pub trait CloacinaPlugin: Send + Sync {
714 /// Returns metadata about all tasks in this workflow package.
715 /// Method index 0.
716 fn get_task_metadata(&self) -> Result<PackageTasksMetadata, PluginError>;
717
718 /// Executes a task by name with the given context.
719 /// Method index 1.
720 fn execute_task(
721 &self,
722 request: TaskExecutionRequest,
723 ) -> Result<TaskExecutionResult, PluginError>;
724
725 /// Returns metadata about the computation graph in this package.
726 /// Method index 2. Only called when the package declares a CG.
727 /// Packages without a graph return an error or NotImplemented.
728 fn get_graph_metadata(&self) -> Result<GraphPackageMetadata, PluginError>;
729
730 /// Executes the computation graph with the given cache state.
731 /// Method index 3. Only called when the package declares a CG.
732 /// Packages without a graph return an error.
733 fn execute_graph(
734 &self,
735 request: GraphExecutionRequest,
736 ) -> Result<GraphExecutionResult, PluginError>;
737
738 /// Returns metadata about all reactors declared by this package.
739 /// Method index 4. Optional (since version 2): plugins built against
740 /// version-1 hosts return `CallError::NotImplemented`, which the
741 /// reconciler treats as "package declares no reactors". Packages built
742 /// from the unified `cloacina::package!()` shell walk their local
743 /// `inventory::iter::<ReactorEntry>` and project each entry into a
744 /// `ReactorPackageMetadata` value. (T-A — I-0102)
745 #[optional(since = 2)]
746 fn get_reactor_metadata(&self) -> Result<Vec<ReactorPackageMetadata>, PluginError>;
747
748 /// Returns metadata about all triggers declared by this package.
749 /// Method index 5. Optional (since version 2): same NotImplemented
750 /// fallback as `get_reactor_metadata`. The unified `cloacina::package!()`
751 /// shell walks `inventory::iter::<TriggerEntry>`, calls each entry's
752 /// constructor, and queries `poll_interval()` / `cron_expression()` /
753 /// `allow_concurrent()` on the resulting `Arc<dyn Trigger>`. The
754 /// reconciler routes cron-shaped entries (cron_expression present) to
755 /// the cron scheduler and the rest to the runtime trigger registry.
756 /// (T-A — I-0102)
757 #[optional(since = 2)]
758 fn get_trigger_metadata(&self) -> Result<Vec<TriggerPackageMetadata>, PluginError>;
759
760 /// Polls a named trigger across the FFI boundary and returns a wire-
761 /// format `TriggerInvokeResult` describing whether to fire the
762 /// associated workflow. Method index 6. Optional (since version 2):
763 /// the host's `FfiTriggerImpl` adapter calls this on every scheduled
764 /// poll for triggers that came from a packaged cdylib (where
765 /// `inventory` doesn't span linker boundaries, so the host can't
766 /// build a host-side `Arc<dyn Trigger>` directly). Plugins built
767 /// from the unified `cloacina::package!()` shell walk
768 /// `inventory::iter::<TriggerEntry>` for the matching name, call
769 /// the constructor, and dispatch `Trigger::poll()` through the
770 /// shared cdylib tokio runtime.
771 #[optional(since = 2)]
772 fn invoke_trigger_poll(
773 &self,
774 request: TriggerInvokeRequest,
775 ) -> Result<TriggerInvokeResult, PluginError>;
776
777 /// Returns metadata about every trigger-less computation graph
778 /// declared by this package. Method index 7. Optional (since
779 /// version 2): plugins built before T-0553's follow-up gap-close
780 /// return `CallError::NotImplemented`, which the reconciler treats
781 /// as "package declares no trigger-less graphs". The unified
782 /// `cloacina::package!()` shell walks
783 /// `inventory::iter::<TriggerlessGraphEntry>` and projects each
784 /// entry's name + terminal_node_names into a
785 /// `TriggerlessGraphMetadataEntry`. Used by
786 /// `step_load_triggerless_cgs` to register host-side
787 /// `TriggerlessGraphRegistration` adapters that dispatch
788 /// invocation through `invoke_triggerless_graph` (method index 8).
789 #[optional(since = 2)]
790 fn get_triggerless_graph_metadata(
791 &self,
792 ) -> Result<Vec<TriggerlessGraphMetadataEntry>, PluginError>;
793
794 /// Invokes a named trigger-less computation graph across the FFI
795 /// boundary and returns a wire-format result. Method index 8.
796 /// Optional (since version 2): the host's `FfiTriggerlessGraph`
797 /// adapter calls this on every workflow-task invocation for graphs
798 /// that came from a packaged cdylib. The shell walks
799 /// `inventory::iter::<TriggerlessGraphEntry>` for the matching
800 /// name, calls the constructor, and dispatches `graph_fn(ctx)` on
801 /// the cdylib's shared tokio runtime.
802 #[optional(since = 2)]
803 fn invoke_triggerless_graph(
804 &self,
805 request: TriggerlessGraphInvokeRequest,
806 ) -> Result<TriggerlessGraphInvokeResult, PluginError>;
807}