Skip to main content

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}