vtx_cli/
packager.rs

1use anyhow::{Context, Result};
2use colored::*;
3use std::path::{Path, PathBuf};
4use wasmparser::{Chunk, Encoding, Parser as WasmParser, Payload};
5use wit_component::ComponentEncoder;
6
7use wasi_preview1_component_adapter_provider::{
8    WASI_SNAPSHOT_PREVIEW1_ADAPTER_NAME, WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
9};
10
11/// Core packaging flow: Wasm -> VTX Component.
12///
13/// Flow:
14/// 1. Read raw Wasm bytes.
15/// 2. Strip non-essential metadata.
16/// 3. Check user imports (warnings only).
17/// 4. Inject Reactor Adapter.
18/// 5. Encode into WebAssembly Component Model.
19/// 6. Validate exports contract.
20///
21/// Parameters:
22/// - `input_wasm_path`: Raw Wasm file path.
23/// - `debug`: Whether to emit verbose logs.
24/// - `force`: Whether to continue on contract validation failures.
25pub fn process_wasm(input_wasm_path: &Path, debug: bool, force: bool) -> Result<Vec<u8>> {
26    let module_bytes = std::fs::read(input_wasm_path).with_context(|| {
27        format!(
28            "Failed to read raw wasm from: {}",
29            input_wasm_path.display()
30        )
31    })?;
32
33    // Fast path: already a component, skip adapter injection and encoding.
34    if is_component(&module_bytes)
35        .with_context(|| "Failed to parse wasm header for component detection")?
36    {
37        println!(
38            "{} Input is already a WebAssembly component; skipping adapter injection and encoding.",
39            "[INFO]".cyan()
40        );
41
42        validate_contract_with_force(&module_bytes, debug, force)?;
43
44        return Ok(module_bytes);
45    }
46
47    // Step 1: metadata cleanup.
48    // The cleaned module represents the user's compiled core logic.
49    let cleaned_module = strip_exports_removed_bindgen_section(&module_bytes)?;
50
51    // Step 2: dependency safety scan (Import Check).
52    // Even with force=false, this only warns to keep builds open.
53    validate_user_imports(&cleaned_module, debug);
54
55    // Step 3: adapter injection.
56    // VTX plugins must run in reactor mode, so inject the reactor adapter.
57    let adapter_bytes = WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
58    if debug {
59        println!("{} Injecting WASI Reactor Adapter", "[DEBUG]".dimmed());
60    }
61
62    // Step 4: component encoding.
63    let component_bytes = ComponentEncoder::default()
64        .module(&cleaned_module)
65        .context("Failed to encode module into component")?
66        .adapter(WASI_SNAPSHOT_PREVIEW1_ADAPTER_NAME, adapter_bytes)
67        .context("Failed to inject WASI preview1 adapter")?
68        .validate(true)
69        .encode()
70        .map_err(|e| {
71            anyhow::anyhow!(
72                "Component encoding error: {e}\nEnsure wit-bindgen version matches adapter requirements."
73            )
74        })?;
75
76    // Step 5: contract validation (Export Check).
77    // Ensure the generated component matches VTX Kernel interfaces.
78    validate_contract_with_force(&component_bytes, debug, force)?;
79
80    Ok(component_bytes)
81}
82
83/// Write a VTX format file.
84pub fn write_vtx_file(
85    input_path: &Path,
86    component_bytes: &[u8],
87    metadata_json: &[u8],
88) -> Result<PathBuf> {
89    let out_path = input_path.with_extension("vtx");
90    let buf = vtx_format::encode_v2(component_bytes, metadata_json);
91
92    std::fs::write(&out_path, buf)
93        .with_context(|| format!("Failed to write vtx artifact: {}", out_path.display()))?;
94
95    Ok(out_path)
96}
97
98// --- Internal helpers ---
99
100/// Validate that user module imports are in the trusted allowlist.
101///
102/// Purpose:
103/// Detect host function dependencies that the kernel may not support.
104/// Use a trust-but-verify approach and warn on unknown imports.
105fn validate_user_imports(module_bytes: &[u8], debug: bool) {
106    let parser = WasmParser::new(0);
107
108    // Trusted namespace prefixes for import modules.
109    // Any import module starting with these is considered safe or adapter-handled.
110    let trusted_namespaces = [
111        "wasi_snapshot_preview1", // Standard WASI Preview 1.
112        "wasi:",                  // Standard WASI (Component Model).
113        "vtx:",                   // VTX SDK official interface.
114        "vtx",                    // VTX SDK legacy compatibility.
115        "__wbindgen_",            // Rust wasm-bindgen internal intrinsics.
116    ];
117
118    for payload in parser.parse_all(module_bytes).flatten() {
119        if let Payload::ImportSection(reader) = payload {
120            for import in reader.into_iter().flatten() {
121                let module = import.module;
122                let field = import.name;
123
124                // Check if the module is trusted.
125                let is_trusted = trusted_namespaces.iter().any(|ns| module.starts_with(ns));
126
127                if !is_trusted {
128                    println!(
129                        "{} Unknown Import Detected: '{}::{}'\n  \
130                            {} This interface is not part of the standard VTX Kernel or WASI spec.\n  \
131                            If the kernel does not provide this host function, the plugin will crash at runtime.",
132                        "[WARN]".yellow(),
133                        module,
134                        field,
135                        "->".yellow()
136                    );
137                } else if debug {
138                    println!(
139                        "{} Trusted import: {}::{}",
140                        "[DEBUG]".dimmed(),
141                        module,
142                        field
143                    );
144                }
145            }
146        }
147    }
148}
149
150fn validate_contract_with_force(component_bytes: &[u8], debug: bool, force: bool) -> Result<()> {
151    if let Err(e) = validate_contract(component_bytes, debug) {
152        if force {
153            println!(
154                "{} Contract validation failed but --force is enabled: {}",
155                "[WARN]".yellow(),
156                e
157            );
158            return Ok(());
159        }
160        return Err(e);
161    }
162
163    Ok(())
164}
165
166/// Determine whether the input is already a WebAssembly Component.
167fn is_component(bytes: &[u8]) -> Result<bool> {
168    let parser = WasmParser::new(0);
169
170    for payload in parser.parse_all(bytes) {
171        let payload = payload?;
172        if let Payload::Version { encoding, .. } = payload {
173            return Ok(matches!(encoding, Encoding::Component));
174        }
175    }
176
177    Ok(false)
178}
179
180/// Validate that the generated component exports required kernel interfaces.
181///
182/// Checks:
183/// 1. Export `handle` (HTTP entrypoint).
184/// 2. Export `get-manifest` (metadata entrypoint).
185fn validate_contract(component_bytes: &[u8], debug: bool) -> Result<()> {
186    let parser = WasmParser::new(0);
187    let mut found_handle = false;
188    let mut found_manifest = false;
189    let mut found_capabilities = false;
190
191    // Parse component exports.
192    for payload in parser.parse_all(component_bytes).flatten() {
193        if let Payload::ComponentExportSection(reader) = payload {
194            for export in reader {
195                let export = export?;
196                // Access the first tuple field to get the name.
197                let name = export.name.0;
198
199                if debug {
200                    println!("{} Found export: {}", "[DEBUG]".dimmed(), name);
201                }
202
203                // Check WIT-defined entrypoints.
204                // These names map to exports in the SDK `world plugin` definition.
205                match name {
206                    "handle" | "vtx:api/plugin/handle" | "vtx:api/plugin#handle" => {
207                        found_handle = true
208                    }
209                    "get-manifest"
210                    | "vtx:api/plugin/get-manifest"
211                    | "vtx:api/plugin#get-manifest" => found_manifest = true,
212                    "get-capabilities"
213                    | "vtx:api/plugin/get-capabilities"
214                    | "vtx:api/plugin#get-capabilities" => found_capabilities = true,
215                    _ => {}
216                }
217            }
218        }
219    }
220
221    if !found_handle {
222        anyhow::bail!("Contract Violation: Missing required export 'handle'.\nEnsure you have implemented the Plugin trait and used 'vtx_sdk::export_plugin!(...)' macro.");
223    }
224    if !found_manifest {
225        anyhow::bail!("Contract Violation: Missing required export 'get-manifest'.");
226    }
227    if !found_capabilities {
228        anyhow::bail!("Contract Violation: Missing required export 'get-capabilities'.");
229    }
230
231    if debug {
232        println!("{} Contract validation passed.", "[INFO]".cyan());
233    }
234
235    Ok(())
236}
237
238/// Remove specific custom sections generated by wit-bindgen.
239fn strip_exports_removed_bindgen_section(module: &[u8]) -> Result<Vec<u8>> {
240    let mut out = Vec::with_capacity(module.len());
241    let mut parser = WasmParser::new(0);
242    let mut offset = 0usize;
243
244    while offset < module.len() {
245        let chunk = parser.parse(&module[offset..], true)?;
246        let (consumed, payload) = match chunk {
247            Chunk::Parsed { consumed, payload } => (consumed, payload),
248            _ => break,
249        };
250
251        let raw = &module[offset..offset + consumed];
252        let mut keep = true;
253
254        if let Payload::CustomSection(cs) = &payload {
255            let name = cs.name();
256            if name.starts_with("component-type:wit-bindgen:")
257                && name.contains("with-all-of-its-exports-removed")
258            {
259                keep = false;
260            }
261        }
262
263        if keep {
264            out.extend_from_slice(raw);
265        }
266        offset += consumed;
267    }
268
269    Ok(out)
270}