Skip to main content

rpc/natives/
mod.rs

1//! Canonical host-fn registry for the 32 server commands.
2//!
3//! Each domain owns a submodule that mirrors the `server::command::*` shape and
4//! holds one file per server command. Commands register their host fn through a
5//! per-domain `register` entry point; the top-level `register_all` aggregates
6//! every domain so a wasmtime [`Linker`](wasmtime::Linker) gains the full
7//! canonical surface in one call.
8//!
9//! Subsequent commits fill each domain. This commit lays down the directory
10//! shape so per-command commits stay focused on one server command at a time.
11
12use wasmtime::{ArrayRef, Linker, Rooted};
13
14use crate::session::SessionData;
15
16/// One stringly-typed wasm host-fn argument as it lands on the
17/// `linker.func_wrap_async` closure: nullable because the arg can be
18/// absent, `Rooted<ArrayRef>` because the wasm side encodes strings as
19/// `(ref null $i8_array)`.
20pub(crate) type StringArg = Option<Rooted<ArrayRef>>;
21/// Three-arg tuple shape used by `account_set_account_tag` and other
22/// host fns that take three string args.
23pub(crate) type StringArgTriple = (StringArg, StringArg, StringArg);
24
25pub mod account;
26pub mod catch_each;
27pub mod commodity;
28pub mod config;
29pub mod env_io;
30mod generated_specs;
31pub mod meta;
32pub mod raise;
33mod render;
34pub mod report;
35pub mod split;
36pub mod ssh_key;
37pub mod template;
38pub mod transaction;
39pub mod user;
40
41pub use generated_specs::all_compiler_specs;
42pub use render::{RENDER_NATIVE_ALLOWLIST, link_render, render_compiler_specs};
43
44/// Aggregator: registers every domain's host fns on `linker` in one call.
45/// Bound to [`SessionData`] now that `meta` natives consult `ScriptCtx`
46/// (user_id) — generic-T register fns from the empty per-domain modules
47/// still satisfy this concrete type because they don't touch the Store data.
48pub fn link(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
49    meta::register(linker)?;
50    env_io::register(linker)?;
51    raise::register(linker)?;
52    catch_each::register(linker)?;
53    account::register(linker)?;
54    commodity::register(linker)?;
55    config::register(linker)?;
56    report::register(linker)?;
57    split::register(linker)?;
58    ssh_key::register(linker)?;
59    template::register(linker)?;
60    transaction::register(linker)?;
61    user::register(linker)?;
62    Ok(())
63}
64
65// `all_compiler_specs` lives in `generated_specs` (tangled from
66// `doc/scripting/native_reference.org` by `rpc/build.rs`) — see the
67// `pub use` import above. Adding or modifying a native means editing
68// the org table; cargo build regenerates the Rust.
69
70#[must_use]
71pub fn all_registered_commands() -> Vec<&'static str> {
72    [
73        account::REGISTERED_COMMANDS,
74        commodity::REGISTERED_COMMANDS,
75        config::REGISTERED_COMMANDS,
76        report::REGISTERED_COMMANDS,
77        split::REGISTERED_COMMANDS,
78        ssh_key::REGISTERED_COMMANDS,
79        transaction::REGISTERED_COMMANDS,
80        user::REGISTERED_COMMANDS,
81    ]
82    .iter()
83    .flat_map(|d| d.iter().copied())
84    .collect()
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn registry_lists_every_planned_command() {
93        let names = all_registered_commands();
94        assert_eq!(names.len(), 36, "command count drifted: {names:?}");
95    }
96
97    #[test]
98    fn registry_uses_kebab_case_only() {
99        for name in all_registered_commands() {
100            assert!(
101                name.chars()
102                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
103                "non-kebab-case name: {name}"
104            );
105            assert!(!name.starts_with('-'), "leading dash: {name}");
106            assert!(!name.ends_with('-'), "trailing dash: {name}");
107        }
108    }
109
110    #[test]
111    fn registry_has_no_duplicates() {
112        let names = all_registered_commands();
113        let mut sorted = names.clone();
114        sorted.sort_unstable();
115        sorted.dedup();
116        assert_eq!(
117            names.len(),
118            sorted.len(),
119            "duplicate command names: {names:?}"
120        );
121    }
122
123    #[test]
124    fn registry_excludes_add_ssh_key_by_design() {
125        let names = all_registered_commands();
126        assert!(
127            !names.contains(&"add-ssh-key"),
128            "add-ssh-key must not be exposed via the eval channel; pubkey upload uses the dedicated ssh-copy-id exec path"
129        );
130    }
131
132    #[test]
133    fn each_domain_has_at_least_one_command() {
134        assert!(!account::REGISTERED_COMMANDS.is_empty());
135        assert!(!commodity::REGISTERED_COMMANDS.is_empty());
136        assert!(!config::REGISTERED_COMMANDS.is_empty());
137        assert!(!report::REGISTERED_COMMANDS.is_empty());
138        assert!(!split::REGISTERED_COMMANDS.is_empty());
139        assert!(!ssh_key::REGISTERED_COMMANDS.is_empty());
140        assert!(!transaction::REGISTERED_COMMANDS.is_empty());
141        assert!(!user::REGISTERED_COMMANDS.is_empty());
142    }
143
144    #[test]
145    fn link_succeeds_against_session_data_linker() {
146        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
147            .expect("engine");
148        let mut linker: wasmtime::Linker<crate::session::SessionData> =
149            wasmtime::Linker::new(&engine);
150        link(&mut linker).expect("link must succeed");
151    }
152
153    #[test]
154    fn each_empty_server_domain_register_succeeds_in_isolation() {
155        // Domains that haven't shipped real natives yet keep their `register`
156        // fns generic over T so empty composition is cheap to verify. As
157        // domains gain real natives (each one becoming SessionData-specific),
158        // they migrate out of this loop into dedicated tests.
159        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
160            .expect("engine");
161        // All domains now have SessionData-specific natives. Nothing left
162        // in the generic-T empty-domain loop, but keep the test placeholder
163        // so the next stub-domain (if any) has an obvious home to slot
164        // back into.
165        let _engine = engine;
166    }
167
168    #[test]
169    fn account_register_succeeds_against_session_data() {
170        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
171            .expect("engine");
172        let mut linker: wasmtime::Linker<crate::session::SessionData> =
173            wasmtime::Linker::new(&engine);
174        account::register(&mut linker).expect("account::register must succeed");
175    }
176
177    #[test]
178    fn commodity_register_succeeds_against_session_data() {
179        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
180            .expect("engine");
181        let mut linker: wasmtime::Linker<crate::session::SessionData> =
182            wasmtime::Linker::new(&engine);
183        commodity::register(&mut linker).expect("commodity::register must succeed");
184    }
185
186    #[test]
187    fn transaction_register_succeeds_against_session_data() {
188        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
189            .expect("engine");
190        let mut linker: wasmtime::Linker<crate::session::SessionData> =
191            wasmtime::Linker::new(&engine);
192        transaction::register(&mut linker).expect("transaction::register must succeed");
193    }
194
195    #[test]
196    fn report_register_succeeds_against_session_data() {
197        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
198            .expect("engine");
199        let mut linker: wasmtime::Linker<crate::session::SessionData> =
200            wasmtime::Linker::new(&engine);
201        report::register(&mut linker).expect("report::register must succeed");
202    }
203
204    #[test]
205    fn user_register_succeeds_against_session_data() {
206        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
207            .expect("engine");
208        let mut linker: wasmtime::Linker<crate::session::SessionData> =
209            wasmtime::Linker::new(&engine);
210        user::register(&mut linker).expect("user::register must succeed");
211    }
212
213    #[test]
214    fn split_register_succeeds_against_session_data() {
215        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
216            .expect("engine");
217        let mut linker: wasmtime::Linker<crate::session::SessionData> =
218            wasmtime::Linker::new(&engine);
219        split::register(&mut linker).expect("split::register must succeed");
220    }
221
222    #[test]
223    fn ssh_key_register_succeeds_against_session_data() {
224        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
225            .expect("engine");
226        let mut linker: wasmtime::Linker<crate::session::SessionData> =
227            wasmtime::Linker::new(&engine);
228        ssh_key::register(&mut linker).expect("ssh_key::register must succeed");
229    }
230
231    #[test]
232    fn config_register_succeeds_against_session_data() {
233        // config now ships server-backed natives so its register fn is
234        // SessionData-specific. Verified in isolation alongside the empty
235        // domains. Async engine required because the host fns are
236        // registered via func_wrap_async.
237        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
238            .expect("engine");
239        let mut linker: wasmtime::Linker<crate::session::SessionData> =
240            wasmtime::Linker::new(&engine);
241        config::register(&mut linker).expect("config::register must succeed");
242    }
243}