Skip to main content

plugin_host/
verify.rs

1//! Wasm-бекенд поверхні `verify` (`wit/verify.wit`, world `verify-plugin`)
2//! — друга реалізація [`harness::registry::VerifyFn`].
3//!
4//! `wit/verify.wit` document-ує, що ЦЕ покриває лише 56/58 виміряних
5//! native-концернів `rules-core` — 2 (`kubeconform`/`kubescape`) спавнять
6//! зовнішній бінарник і wasm-компонентом узагалі не можуть бути (guest не
7//! спавнить процесів, доккоментар `crate` "жодного спавну процесів"); для
8//! них `verify` лишається host-інʼєкцією через in-process бекенд
9//! `harness::registry`, не через цей файл.
10
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14use wasmtime::component::{Component, HasSelf, Linker, ResourceTable};
15use wasmtime::{Engine, Store};
16
17use harness::registry::VerifyFn;
18use llm_lib::attempt::VerifyReport;
19
20use crate::engine::new_engine;
21use crate::host_state::HostState;
22use crate::wasi_ctx::no_filesystem_ctx;
23
24mod bindings {
25    wasmtime::component::bindgen!({
26        path: "wit",
27        world: "verify-plugin",
28        // WASI p3 побудована на `component-model-async`, і синхронного
29        // лінкера в `wasmtime_wasi::p3` не існує — тож і згенерований
30        // виклик, і хостові імпорти мусять бути async (доккоментар
31        // `engine.rs`).
32        imports: { default: async },
33        exports: { default: async },
34    });
35}
36
37/// Дзеркало `record verify-report` → [`VerifyReport`] — доккоментар
38/// `detector.rs::From<...Violation> for Violation` пояснює, чому це
39/// РЕАЛЬНИЙ конвертер `bindgen!`-типу, не ручне дзеркало.
40impl From<bindings::exports::n7n::fix_deps::verify::VerifyReport> for VerifyReport {
41    fn from(r: bindings::exports::n7n::fix_deps::verify::VerifyReport) -> Self {
42        VerifyReport {
43            ok: r.ok,
44            output: r.output,
45            infra_error: r.infra_error,
46        }
47    }
48}
49
50impl bindings::n7n::fix_deps::host_fs::Host for HostState {
51    async fn read_file(&mut self, path: String) -> Result<String, String> {
52        std::fs::read_to_string(self.repo_root.join(path)).map_err(|e| e.to_string())
53    }
54}
55
56/// Wasm-бекенд одного `verify`-компонента — доккоментар [`crate::detector::WasmDetector`]
57/// пояснює кешування `Component`/`Engine`/`Linker` проти "інстанс на
58/// спробу" для `Instance`.
59pub struct WasmVerify {
60    engine: Engine,
61    component: Component,
62    linker: Linker<HostState>,
63    repo_root: PathBuf,
64}
65
66impl WasmVerify {
67    /// Доккоментар [`crate::detector::WasmDetector::from_file`] — та сама
68    /// побудова `Engine`/`Component`/`Linker`, лише інший `world`.
69    pub fn from_file(
70        repo_root: impl Into<PathBuf>,
71        wasm_path: impl AsRef<Path>,
72    ) -> wasmtime::Result<Self> {
73        let engine = new_engine()?;
74        let component = Component::from_file(&engine, wasm_path.as_ref())?;
75        let mut linker = Linker::<HostState>::new(&engine);
76        wasmtime_wasi::p3::add_to_linker(&mut linker)?;
77        bindings::n7n::fix_deps::host_fs::add_to_linker::<_, HasSelf<HostState>>(
78            &mut linker,
79            |s| s,
80        )?;
81        Ok(Self {
82            engine,
83            component,
84            linker,
85            repo_root: repo_root.into(),
86        })
87    }
88
89    async fn call(&self) -> wasmtime::Result<VerifyReport> {
90        let wasi = no_filesystem_ctx();
91        let mut store = Store::new(
92            &self.engine,
93            HostState {
94                wasi,
95                table: ResourceTable::new(),
96                repo_root: self.repo_root.clone(),
97            },
98        );
99        let instance =
100            bindings::VerifyPlugin::instantiate_async(&mut store, &self.component, &self.linker)
101                .await?;
102        let report = instance.n7n_fix_deps_verify().call_run(&mut store).await?;
103        Ok(VerifyReport::from(report))
104    }
105
106    /// Перетворює на [`VerifyFn`]. `verify.wit` (`run: func() ->
107    /// verify-report`, без `result`) не має каналу для "сам виклик упав" —
108    /// той самий брак, що вже несе Rust-сигнатура
109    /// (`Arc<dyn Fn() -> BoxFuture<'static, VerifyReport>>`, без `Result`).
110    /// Інфраструктурний збій викликового шляху (інстанціювання, трап)
111    /// мапиться в `infra_error: true` — рівно те поле, яке `attempt.rs`
112    /// документує як "не результат «червоно»": rig списує хід завжди,
113    /// облік такого випадку — наш.
114    #[must_use]
115    pub fn into_verify_fn(self) -> VerifyFn {
116        let this = Arc::new(self);
117        Arc::new(move || {
118            let this = Arc::clone(&this);
119            Box::pin(async move {
120                this.call().await.unwrap_or_else(|err| VerifyReport {
121                    ok: false,
122                    output: format!("wasm-компонент verify: {err}"),
123                    infra_error: true,
124                })
125            })
126        })
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    //! Доккоментар `detector.rs::tests` — той самий компіляційний доказ
133    //! без `.wasm`, тут для `verify-report`.
134    use super::*;
135
136    #[test]
137    fn bindgen_verify_report_converts_to_llm_lib_verify_report_field_by_field() {
138        let wit_report = bindings::exports::n7n::fix_deps::verify::VerifyReport {
139            ok: false,
140            output: "діагностика".to_string(),
141            infra_error: true,
142        };
143
144        let report = VerifyReport::from(wit_report);
145
146        assert!(!report.ok);
147        assert_eq!(report.output, "діагностика");
148        assert!(report.infra_error);
149    }
150}