Lines
93.94 %
Functions
14.58 %
Branches
100 %
//! Template render entry point.
//!
//! A template is per-user nomiscript source that pre-fills a transaction draft.
//! [`compile_template`] validates source against the restricted render compiler
//! surface (a template naming any non-allowlisted native is a compile error).
//! [`render_template`] runs it under [`link_render`](crate::natives::link_render)
//! — a linker that registers only the read-only financial natives and the draft
//! natives, never the config/user/ssh secret surface — and returns the
//! accumulated [`TransactionDraft`].
//! Both gates (compiler whitelist + linker omission) are load-bearing for token
//! security: Slice B keeps the per-user JWT private key in the same per-user DB
//! the eval channel reads, so an escape here would let a template exfiltrate the
//! signing key. The whitelist test suite (`tests/`) is treated as a security
//! gate.
use std::sync::{Arc, Mutex};
use nomiscript::{Compiler, Program, Reader, SymbolTable};
use scripting::runtime::{EngineError, classify_runtime_error};
use thiserror::Error;
use wasmtime::{AnyRef, Linker, Rooted, Store, Val};
use crate::ctx::ScriptCtx;
use crate::draft::TransactionDraft;
use crate::session::SessionData;
use crate::wasm::{EngineOpts, build_engine};
const RENDER_EPOCH_TICKS: u64 = 1;
#[derive(Debug, Error)]
pub enum TemplateError {
/// Source failed to parse or referenced a native outside the render
/// whitelist (or any other compile error).
#[error("template compile error: {0}")]
Compile(String),
/// The wasm engine / instantiation failed.
#[error("template engine error: {0}")]
Engine(String),
/// A draft native trapped at runtime (bad arg, missing entity, …).
#[error("template render error: {0}")]
Runtime(String),
}
/// Compiles `source` against the restricted render surface, returning the wasm
/// bytes. A template that names a non-allowlisted native fails here with a
/// [`TemplateError::Compile`] — this is the first half of the security gate
/// (the compiler simply doesn't know the dangerous natives exist).
pub fn compile_template(source: &str) -> Result<Vec<u8>, TemplateError> {
let (_, bytes) = compile_render_program(source)?;
Ok(bytes)
fn compile_render_program(source: &str) -> Result<(Program, Vec<u8>), TemplateError> {
let host_fns = crate::natives::render_compiler_specs();
let mut symbols = SymbolTable::with_builtins();
symbols.register_host_fns(&host_fns);
crate::host_prelude::load(&mut symbols);
let mut compiler = Compiler::with_host_fns(host_fns);
let program = Reader::parse(source).map_err(|err| TemplateError::Compile(err.to_string()))?;
let (bytes, _ty) = compiler
.compile_eval_with_type(&program, &mut symbols)
.map_err(|err| TemplateError::Compile(err.to_string()))?;
Ok((program, bytes))
/// Renders `source` for `ctx`'s user, returning the accumulated draft. Runs the
/// program form-by-form under the render linker (read-only financial natives +
/// draft natives only). The final value of each form is ignored; only the draft
/// side effects matter.
pub async fn render_template(
ctx: &ScriptCtx,
source: &str,
) -> Result<TransactionDraft, TemplateError> {
let engine = build_engine(EngineOpts::baseline().with_fuel())
.map_err(|err| TemplateError::Engine(err.to_string()))?;
let mut linker: Linker<SessionData> = Linker::new(&engine);
crate::natives::link_render(&mut linker)
let output = Arc::new(Mutex::new(String::new()));
let mut store: Store<SessionData> =
Store::new(&engine, SessionData::for_render(ctx.clone(), output));
store
.set_fuel(ctx.limits.fuel)
store.set_epoch_deadline(RENDER_EPOCH_TICKS);
for form in program.exprs {
let single = Program::new(vec![form]);
.compile_eval_with_type(&single, &mut symbols)
let module = scripting::runtime::compile_module(&engine, &bytes)
let instance = linker
.instantiate_async(&mut store, &module)
.await
.map_err(|err| TemplateError::Engine(classify_engine(&err)))?;
let func = instance.get_func(&mut store, "nomi-eval").ok_or_else(|| {
TemplateError::Engine("render module missing nomi-eval export".to_string())
})?;
let mut results = [Val::AnyRef(None)];
func.call_async(&mut store, &[], &mut results)
.map_err(|err| TemplateError::Runtime(classify_engine(&err)))?;
let _: Option<Rooted<AnyRef>> = match results[0] {
Val::AnyRef(a) => a,
_ => None,
};
Ok(store.into_data().into_draft().unwrap_or_default())
fn classify_engine(err: &wasmtime::Error) -> String {
match classify_runtime_error(err) {
EngineError::ScriptRaised { code, message } => format!("{code}: {message}"),
other => other.to_string(),