cpex_plugin_pii_scanner/factory.rs
1// Location: ./builtins/plugins/pii-scanner/src/factory.rs
2// Copyright 2026
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5
6use std::sync::Arc;
7
8use cpex_core::{
9 cmf::CmfHook,
10 error::PluginError,
11 factory::{PluginFactory, PluginInstance},
12 hooks::TypedHandlerAdapter,
13 plugin::PluginConfig,
14};
15
16use crate::scanner::PiiScanner;
17
18/// `kind:` string operators write in CPEX YAML to declare a PII
19/// scanner instance.
20pub const KIND: &str = "validator/pii-scan";
21
22/// Factory for `kind: validator/pii-scan`. Instantiates a
23/// `PiiScanner` from the `config:` block and registers a handler
24/// for every CMF hook name listed in `cfg.hooks`. Operators
25/// typically wire it on `cmf.tool_pre_invoke` /
26/// `cmf.prompt_pre_invoke` / `cmf.resource_pre_fetch` so it runs
27/// before any of those entity types reach the backend.
28pub struct PiiScannerFactory;
29
30impl PluginFactory for PiiScannerFactory {
31 fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>> {
32 let scanner = Arc::new(PiiScanner::new(config.clone())?);
33
34 // Register the same handler instance against every CMF hook
35 // name the operator declared in YAML — same plugin, multiple
36 // entry points. Empty hooks list is a config error.
37 if config.hooks.is_empty() {
38 return Err(Box::new(PluginError::Config {
39 message: format!(
40 "plugin '{}' (cpex-plugin-pii-scanner): `hooks:` must list at \
41 least one CMF hook to scan on (e.g. cmf.tool_pre_invoke)",
42 config.name
43 ),
44 }));
45 }
46
47 let handlers: Vec<_> = config
48 .hooks
49 .iter()
50 .map(|h| -> (&'static str, _) {
51 // Leak the string to get a 'static lifetime — the
52 // handler registry stores it that way for cheap
53 // comparison. PluginConfigs are read once at startup
54 // and live for the process lifetime, so the leak
55 // bound is the number of plugin × hook pairs in
56 // config (small, bounded).
57 let leaked: &'static str = Box::leak(h.clone().into_boxed_str());
58 let adapter: Arc<dyn cpex_core::registry::AnyHookHandler> =
59 Arc::new(TypedHandlerAdapter::<CmfHook, _>::new(Arc::clone(&scanner)));
60 (leaked, adapter)
61 })
62 .collect();
63
64 Ok(PluginInstance {
65 plugin: scanner,
66 handlers,
67 })
68 }
69}