Lines
100 %
Functions
Branches
//! Security gate for the template render surface (Slice D).
//!
//! Templates are per-user nomiscript that must reach NO mutating and NO
//! secret/credential/auth native. Because Slice B keeps the per-user JWT
//! private key in the same per-user DB the eval channel reads, an escape here
//! would let a template exfiltrate the signing key — so these compile-failure
//! tests are a security gate, not mere coverage.
//! The proof is structural and at compile time: `compile_template` validates
//! against `render_compiler_specs`, so a template naming any non-allowlisted
//! native fails to compile (the compiler doesn't know the native exists). Each
//! native asserted-excluded here IS present on the full eval surface
//! (`all_compiler_specs`), so the test proves exclusion, not absence.
use rpc::compile_template;
use rpc::natives::{RENDER_NATIVE_ALLOWLIST, all_compiler_specs, render_compiler_specs};
/// Every native that templates MUST NOT reach. Each is on the full eval surface
/// (verified by `dangerous_natives_are_on_the_full_surface`), so a
/// compile-failure proves the render whitelist excludes a real, callable
/// native — not a typo or a never-registered name.
const DANGEROUS_NATIVES: &[&str] = &[
"get-config", // returns config rows incl. the JWT private key
"set-config", // arbitrary per-user config write
"verify-user-password", // credential brute force
"lookup-user-by-ssh-key", // global fingerprint -> user directory
"list-ssh-keys", // auth surface
"remove-ssh-key", // auth revocation
"create-transaction", // mutator
"update-transaction", // mutator
"delete-transaction", // mutator
"create-account", // mutator
"create-commodity", // mutator
"set-account-tag", // mutator
"set-transaction-tag", // mutator
"set-split-tag", // mutator
];
fn nomi_names(specs: &[nomiscript::HostFnSpec]) -> Vec<String> {
specs
.iter()
.map(|s| s.nomi_name.to_ascii_lowercase())
.collect()
}
#[test]
fn dangerous_natives_are_on_the_full_surface() {
let full = nomi_names(&all_compiler_specs());
for name in DANGEROUS_NATIVES {
assert!(
full.contains(&name.to_string()),
"test premise broken: '{name}' is not on the full eval surface, \
so excluding it from render proves nothing"
);
fn render_specs_exclude_every_dangerous_native() {
let render = nomi_names(&render_compiler_specs());
!render.contains(&name.to_string()),
"render surface must not expose '{name}'"
fn render_specs_are_exactly_the_allowlist() {
let mut render = nomi_names(&render_compiler_specs());
render.sort();
let mut allow: Vec<String> = RENDER_NATIVE_ALLOWLIST
.map(|s| s.to_string())
.collect();
allow.sort();
assert_eq!(
render, allow,
"render specs must equal the allowlist — a drift means a native \
leaked in or an expected one dropped out"
fn template_calling_get_config_fails_to_compile() {
let err = compile_template("(get-config \"access_token_private_key\")")
.expect_err("get-config must not compile in a template");
assert!(format!("{err}").to_lowercase().contains("compile"));
fn template_calling_verify_user_password_fails_to_compile() {
compile_template("(verify-user-password \"a@b.c\" \"pw\")")
.expect_err("verify-user-password must not compile in a template");
fn template_calling_lookup_user_by_ssh_key_fails_to_compile() {
compile_template("(lookup-user-by-ssh-key \"SHA256:abc\")")
.expect_err("lookup-user-by-ssh-key must not compile in a template");
fn template_calling_create_transaction_fails_to_compile() {
compile_template("(create-transaction \"x\")")
.expect_err("create-transaction must not compile in a template");
fn template_calling_set_config_fails_to_compile() {
compile_template("(set-config \"k\" \"v\")")
.expect_err("set-config must not compile in a template");
fn template_with_only_draft_and_reads_compiles() {
// Pure-shape template: a draft note plus a read native. No DB is touched
// (compile only), so this proves the allowlisted surface is usable.
compile_template(
"(set-draft-note \"Groceries\")\n\
(set-draft-date \"2026-06-15\")\n\
(draft-tag \"category\" \"food\")",
)
.expect("a template using only draft natives must compile");
fn draft_natives_are_present_on_render_surface() {
for n in [
"set-draft-note",
"set-draft-date",
"draft-split",
"draft-tag",
] {
assert!(render.contains(&n.to_string()), "render must expose '{n}'");