plugin_host/lib.rs
1//! wasmtime-бекенд `wit/*.wit`-контрактів (`n7n:fix-deps@0.1.0`) — спека
2//! `2026-08-17-n7n-harness-local-models.md`, §3.12 пункт 16, ДРУГА половина
3//! (пункт 16а — контракти й in-process-бекенд, `harness::registry` — уже
4//! зроблений, без wasmtime; цей крейт додає ЄДИНУ нову залежність, яку
5//! той крок свідомо не чіпав).
6//!
7//! # Чому окремий крейт, а не `harness`/`llm-lib`
8//!
9//! `wasmtime` — найбільша залежність усього плану `FixDeps`-плагінів.
10//! `harness` тримає інваріант «нуль важких залежностей» (CI перевіряє
11//! `cargo tree -p n7n-harness --all-features` — там немає `wasmtime`); і
12//! `n7n-llm-lib` (featureless-ядро) щойно спеціально звужували (§3.1).
13//! Обидва інваріанти цей крейт **не чіпає** — він лише СПОЖИВАЄ обидва
14//! (`n7n-harness` за типами `DetectFn`/`T0Fn`/`Violation`/`VerifyFn`/
15//! `AstFactsFn` з `crate::registry`/`crate::pipeline`, `n7n-llm-lib` з
16//! `default-features = false` за `EditPlan`/`FileEditPlan`/`VerifyReport`).
17//! Підключає його лише той споживач, що реально вантажить wasm-плагіни
18//! (`rules-fix`) — `harness`/`llm-lib` лишаються придатними для
19//! споживачів, яким wasm не потрібен, без жодного зайвого байта в графі.
20//!
21//! # Що тут головне: чотири `Wasm*` — друга реалізація тих самих слотів
22//!
23//! [`harness::registry::PluginRegistry`] уже спроєктований так, що бекенд
24//! компонента — друга реалізація `T` усередині `Slot<T>`, не окремий
25//! трейт-об'єкт (доккоментар `registry.rs`, "чому `T`, а не трейт"). Чотири
26//! типи нижче — [`WasmDetector`], [`WasmVerify`], [`WasmAstFacts`],
27//! [`WasmT0`] — виробляють РІВНО ті самі callable-типи, що in-process
28//! бекенд уже приймає ([`harness::pipeline::DetectFn`],
29//! [`harness::registry::VerifyFn`], [`harness::registry::AstFactsFn`],
30//! [`harness::pipeline::T0Fn`]) — `PluginRegistry::new`/`replace_*` не
31//! відрізняє, звідки прийшло замикання: з `Arc::new(|| ...)` напряму чи
32//! з `WasmDetector::into_detect_fn()` нижче. Володіння (реєстр, `Slot`,
33//! `generation`, LIFO-аквайр) лишається рівно тим самим кодом
34//! (`crates/harness/src/registry.rs`) — цей крейт лише постачає ЩО саме
35//! кладеться в слот.
36//!
37//! # Безпекова межа (§3.9) — де вона виражена в коді
38//!
39//! - **Guest-и не отримують WASI-права запису взагалі.** Єдине місце, де
40//! guest отримує (чи не отримує) права на диск — [`wasi_ctx::no_filesystem_ctx`]:
41//! ЖОДНОГО preopen, тобто файлової capability немає взагалі. Запис не
42//! "заборонений перевіркою прав" — він невиразимий, бо в таблиці guest-а
43//! немає каталогу, з якого можна почати шлях. Читає guest вужчим
44//! `host-fs.read-file` (§3.9 передбачає цей шлях явно).
45//! Кожен із чотирьох `Wasm*::call*` нижче будує `WasiCtx` РІВНО через цю
46//! функцію — підміни в обхід неї немає, бо `Store` для guest-виклику
47//! ніде більше в крейті не конструюється.
48//! - **T0 повертає `edit-plan`, хост застосовує.** [`WasmT0::into_t0_fn`]
49//! виробляє лише [`harness::pipeline::T0Fn`] (порахувати план) — фаза
50//! застосування (`PreparePlanFn`/`CommitPlanFn`, `write_guard`) сюди НЕ
51//! входить: `n7n-llm-lib` тут узятий з `default-features = false`, тож
52//! `write_guard` (живе під фічею `agents`) цьому крейту недоступний за
53//! побудовою — той самий принцип, що вже застосовує `harness::registry`
54//! (доккоментар `T0Step`: "`prepare`/`commit` — цілком хостові… не
55//! частина контракту «порахувати план»"). Викликач збирає повний
56//! `T0Step { plan: wasm_t0.into_t0_fn(), prepare: ..., commit: ... }`
57//! сам, як і сьогодні для in-process T0.
58//! - **Жодного спавну процесів у guest.** Жоден із чотирьох `wit/*.wit`
59//! world-ів не імпортує нічого, що дало б guest-у спавнити процес
60//! (жодного `wasi:cli/exec`-подібного імпорту в жодному з
61//! `detector-plugin`/`verify-plugin`/`ast-facts-plugin`/`t0-plugin`), і
62//! WASI Preview 2 в принципі не має такого стандартного інтерфейсу —
63//! немає wasi-фічі, яку можна було б увімкнути, щоб це з'явилось.
64//! - **Інстанс на спробу, без кешу.** Кожен `Wasm*::call*` будує СВІЖИЙ
65//! `Store`+`WasiCtx`+`Instance` на КОЖЕН виклик (не лише на кожен
66//! `checkout()` реєстру) — суворіша гарантія, ніж вимагає §3.16 правило
67//! 3 ("інстанс на спробу"): жодного способу випадково закешувати
68//! `Instance` між викликами немає, бо для нього просто немає поля, яке
69//! пережило б виклик. Кешується лише СКОМПІЛЬОВАНИЙ [`wasmtime::component::Component`]
70//! (і `Engine`/`Linker`, які самі по собі не несуть стану guest-а) —
71//! те саме розрізнення "Component ≠ Instance", яке задача явно вимагає.
72//!
73//! # Тестовий guest
74//!
75//! `tests/guest/detector_fixture` і `tests/guest/t0_fixture` — мінімальні
76//! `wasm32-wasip3`-компоненти для наскрізного тесту
77//! (`tests/wasmtime_backend.rs`). Компіляція гейтована наявністю
78//! `wasm32-wasip3` (nightly + `-Z build-std` + wasi-sdk у `WASI_SDK_PATH`) — де їх немає
79//! (чи `cargo build` для guest-крейта впаде з іншої причини, напр. немає
80//! мережі до crates.io), тест друкує причину й виходить РАНО, не падаючи:
81//! середовище цієї задачі мало і target, і мережу (перевірено на старті),
82//! тож тут наскрізний прогін ДІЙСНО виконується, а не лише компілюється
83//! бекенд.
84
85mod ast_facts;
86mod detector;
87mod engine;
88mod host_state;
89mod t0;
90mod verify;
91mod wasi_ctx;
92
93pub use ast_facts::WasmAstFacts;
94pub use detector::WasmDetector;
95pub use t0::WasmT0;
96pub use verify::WasmVerify;