Skip to main content

rpc/
template.rs

1//! Template render entry point.
2//!
3//! A template is per-user nomiscript source that pre-fills a transaction draft.
4//! [`compile_template`] validates source against the restricted render compiler
5//! surface (a template naming any non-allowlisted native is a compile error).
6//! [`render_template`] runs it under [`link_render`](crate::natives::link_render)
7//! — a linker that registers only the read-only financial natives and the draft
8//! natives, never the config/user/ssh secret surface — and returns the
9//! accumulated [`TransactionDraft`].
10//!
11//! Both gates (compiler whitelist + linker omission) are load-bearing for token
12//! security: Slice B keeps the per-user JWT private key in the same per-user DB
13//! the eval channel reads, so an escape here would let a template exfiltrate the
14//! signing key. The whitelist test suite (`tests/`) is treated as a security
15//! gate.
16
17use std::sync::{Arc, Mutex};
18
19use nomiscript::{Compiler, Program, Reader, SymbolTable};
20use scripting::runtime::{EngineError, classify_runtime_error};
21use thiserror::Error;
22use wasmtime::{AnyRef, Linker, Rooted, Store, Val};
23
24use crate::ctx::ScriptCtx;
25use crate::draft::TransactionDraft;
26use crate::session::SessionData;
27use crate::wasm::{EngineOpts, build_engine};
28
29const RENDER_EPOCH_TICKS: u64 = 1;
30
31#[derive(Debug, Error)]
32pub enum TemplateError {
33    /// Source failed to parse or referenced a native outside the render
34    /// whitelist (or any other compile error).
35    #[error("template compile error: {0}")]
36    Compile(String),
37    /// The wasm engine / instantiation failed.
38    #[error("template engine error: {0}")]
39    Engine(String),
40    /// A draft native trapped at runtime (bad arg, missing entity, …).
41    #[error("template render error: {0}")]
42    Runtime(String),
43}
44
45/// Compiles `source` against the restricted render surface, returning the wasm
46/// bytes. A template that names a non-allowlisted native fails here with a
47/// [`TemplateError::Compile`] — this is the first half of the security gate
48/// (the compiler simply doesn't know the dangerous natives exist).
49pub fn compile_template(source: &str) -> Result<Vec<u8>, TemplateError> {
50    let (_, bytes) = compile_render_program(source)?;
51    Ok(bytes)
52}
53
54fn compile_render_program(source: &str) -> Result<(Program, Vec<u8>), TemplateError> {
55    let host_fns = crate::natives::render_compiler_specs();
56    let mut symbols = SymbolTable::with_builtins();
57    symbols.register_host_fns(&host_fns);
58    crate::host_prelude::load(&mut symbols);
59    let mut compiler = Compiler::with_host_fns(host_fns);
60    let program = Reader::parse(source).map_err(|err| TemplateError::Compile(err.to_string()))?;
61    let (bytes, _ty) = compiler
62        .compile_eval_with_type(&program, &mut symbols)
63        .map_err(|err| TemplateError::Compile(err.to_string()))?;
64    Ok((program, bytes))
65}
66
67/// Renders `source` for `ctx`'s user, returning the accumulated draft. Runs the
68/// program form-by-form under the render linker (read-only financial natives +
69/// draft natives only). The final value of each form is ignored; only the draft
70/// side effects matter.
71pub async fn render_template(
72    ctx: &ScriptCtx,
73    source: &str,
74) -> Result<TransactionDraft, TemplateError> {
75    let host_fns = crate::natives::render_compiler_specs();
76    let mut symbols = SymbolTable::with_builtins();
77    symbols.register_host_fns(&host_fns);
78    crate::host_prelude::load(&mut symbols);
79    let mut compiler = Compiler::with_host_fns(host_fns);
80    let program = Reader::parse(source).map_err(|err| TemplateError::Compile(err.to_string()))?;
81
82    let engine = build_engine(EngineOpts::baseline().with_fuel())
83        .map_err(|err| TemplateError::Engine(err.to_string()))?;
84    let mut linker: Linker<SessionData> = Linker::new(&engine);
85    crate::natives::link_render(&mut linker)
86        .map_err(|err| TemplateError::Engine(err.to_string()))?;
87
88    let output = Arc::new(Mutex::new(String::new()));
89    let mut store: Store<SessionData> =
90        Store::new(&engine, SessionData::for_render(ctx.clone(), output));
91    store
92        .set_fuel(ctx.limits.fuel)
93        .map_err(|err| TemplateError::Engine(err.to_string()))?;
94    store.set_epoch_deadline(RENDER_EPOCH_TICKS);
95
96    for form in program.exprs {
97        let single = Program::new(vec![form]);
98        let (bytes, _ty) = compiler
99            .compile_eval_with_type(&single, &mut symbols)
100            .map_err(|err| TemplateError::Compile(err.to_string()))?;
101        let module = scripting::runtime::compile_module(&engine, &bytes)
102            .map_err(|err| TemplateError::Engine(err.to_string()))?;
103        let instance = linker
104            .instantiate_async(&mut store, &module)
105            .await
106            .map_err(|err| TemplateError::Engine(classify_engine(&err)))?;
107        let func = instance.get_func(&mut store, "nomi-eval").ok_or_else(|| {
108            TemplateError::Engine("render module missing nomi-eval export".to_string())
109        })?;
110        let mut results = [Val::AnyRef(None)];
111        func.call_async(&mut store, &[], &mut results)
112            .await
113            .map_err(|err| TemplateError::Runtime(classify_engine(&err)))?;
114        let _: Option<Rooted<AnyRef>> = match results[0] {
115            Val::AnyRef(a) => a,
116            _ => None,
117        };
118    }
119
120    Ok(store.into_data().into_draft().unwrap_or_default())
121}
122
123fn classify_engine(err: &wasmtime::Error) -> String {
124    match classify_runtime_error(err) {
125        EngineError::ScriptRaised { code, message } => format!("{code}: {message}"),
126        other => other.to_string(),
127    }
128}