Skip to main content

rpc/natives/
meta.rs

1//! Diagnostic / session-introspection natives.
2//!
3//! Distinct from the 30 server-command bindings: these expose facts about
4//! the rpc channel itself (protocol version, authenticated user) that don't
5//! pass through `server::command::*`. Useful as a connection sanity check the
6//! emacs client can call on connect to verify it's talking to the right
7//! server, and as the smallest possible end-to-end proof that the host-fn
8//! dispatch path through `SessionData` works before any DB-touching native
9//! lands.
10
11use scripting::runtime::alloc_string_ref;
12use wasmtime::{ArrayRef, Caller, Linker, Rooted};
13
14use crate::session::SessionData;
15
16/// Wire-level protocol revision the server speaks. Bumped when an
17/// incompatible change to the request/response envelope or capture protocol
18/// ships. The emacs client can refuse to talk to a mismatched server.
19pub const PROTOCOL_VERSION: i32 = 1;
20
21/// Names exposed via `(<name>)` in nomiscript. Not counted in the plan's
22/// 30 server-command surface — this is a separate, smaller diagnostic
23/// surface that emacs / cli / tui all share.
24pub const REGISTERED_NATIVES: &[&str] = &["rpc-protocol-version", "rpc-session-user-id"];
25
26/// Returns the `HostFnSpec` list the nomiscript compiler needs in order to
27/// recognise the meta natives during compilation. Mirrors the linker
28/// registrations in [`register`] — both must stay in sync, hence one place.
29pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
30    linker.func_wrap(
31        "nomi",
32        "rpc_protocol_version",
33        |_caller: Caller<'_, SessionData>| -> i32 { PROTOCOL_VERSION },
34    )?;
35    linker.func_wrap(
36        "nomi",
37        "rpc_session_user_id_capture",
38        |mut caller: Caller<'_, SessionData>| -> wasmtime::Result<Option<Rooted<ArrayRef>>> {
39            let uid_string = caller.data().ctx().user_id.to_string();
40            Ok(Some(alloc_string_ref(&mut caller, uid_string.as_bytes())?))
41        },
42    )?;
43    Ok(())
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use crate::ctx::ScriptCtx;
50    use scripting::runtime::{EngineOpts, build_engine, compile_wat};
51    use uuid::Uuid;
52    use wasmtime::Store;
53
54    fn run_module_with_user(uid: Uuid, wat: &str) -> (SessionData, i32) {
55        let engine = build_engine(EngineOpts::baseline().with_fuel()).unwrap();
56        let module = compile_wat(&engine, wat).unwrap();
57        let mut linker: Linker<SessionData> = Linker::new(&engine);
58        register(&mut linker).unwrap();
59        let mut store: Store<SessionData> = Store::new(
60            &engine,
61            SessionData::new(
62                ScriptCtx::new(uid),
63                std::sync::Arc::new(std::sync::Mutex::new(String::new())),
64            ),
65        );
66        store.set_fuel(100_000).unwrap();
67        store.set_epoch_deadline(1);
68        let instance = linker.instantiate(&mut store, &module).unwrap();
69        let func = instance
70            .get_typed_func::<(), i32>(&mut store, "nomi-eval")
71            .unwrap();
72        let result = func.call(&mut store, ()).unwrap();
73        (store.into_data(), result)
74    }
75
76    #[test]
77    fn protocol_version_native_returns_constant() {
78        // Smoke test for `rpc_protocol_version` — wat returns the i32
79        // directly from nomi-eval so the test stays decoupled from the
80        // anyref decoder that the rpc Session uses in production.
81        let wat = r#"
82        (module
83          (import "nomi" "rpc_protocol_version" (func $ver (result i32)))
84          (func (export "nomi-eval") (result i32) (call $ver)))
85        "#;
86        let (_, value) = run_module_with_user(Uuid::nil(), wat);
87        assert_eq!(value, PROTOCOL_VERSION);
88    }
89
90    #[test]
91    fn registered_natives_are_kebab_case() {
92        for name in REGISTERED_NATIVES {
93            assert!(
94                name.chars()
95                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
96                "non-kebab-case meta native: {name}"
97            );
98        }
99    }
100}