From f97daf12579da2d11bfe7fd2f1a49752b3ae9fca Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Mon, 29 Jun 2026 12:06:57 -0600 Subject: [PATCH 1/4] fix(plugin): make native example satisfy trust policy Signed-off-by: Bryan Bednarski --- .../tests/coverage/plugins_lifecycle_tests.rs | 54 +++++++++++++++++++ examples/rust-native-plugin/README.md | 26 ++++++--- examples/rust-native-plugin/relay-plugin.toml | 8 +++ 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/crates/cli/tests/coverage/plugins_lifecycle_tests.rs b/crates/cli/tests/coverage/plugins_lifecycle_tests.rs index 5e57969c..815950f3 100644 --- a/crates/cli/tests/coverage/plugins_lifecycle_tests.rs +++ b/crates/cli/tests/coverage/plugins_lifecycle_tests.rs @@ -351,6 +351,60 @@ symbol = "nemo_relay_fixture_native_plugin" manifest_path } +fn materialize_native_example_manifest(dir: &Path) -> PathBuf { + let artifact_name = "libnemo_relay_rust_native_plugin_example.so"; + let artifact_relative = Path::new("target").join("debug").join(artifact_name); + let artifact_path = dir.join(&artifact_relative); + std::fs::create_dir_all(artifact_path.parent().unwrap()).unwrap(); + let artifact_body = b"native plugin example fixture"; + std::fs::write(&artifact_path, artifact_body).unwrap(); + let digest = Sha256::digest(artifact_body) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + + let repository_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let template = std::fs::read_to_string( + repository_root.join("examples/rust-native-plugin/relay-plugin.toml"), + ) + .unwrap(); + let manifest = template + .replace("", artifact_name) + .replace("", &digest); + let manifest_path = dir.join("relay-plugin.toml"); + std::fs::write(&manifest_path, manifest).unwrap(); + manifest_path +} + +#[test] +fn tracked_native_plugin_example_satisfies_default_trust_policy() { + let temp = tempfile::tempdir().unwrap(); + let _env = EnvScope::hermetic(&temp); + let _cwd = CurrentDirGuard::enter(temp.path()); + let plugin_dir = temp.path().join("plugins").join("native-example"); + std::fs::create_dir_all(&plugin_dir).unwrap(); + materialize_native_example_manifest(&plugin_dir); + + add( + PluginsAddCommand { + scope: PluginsScopeArgs { + project: true, + ..PluginsScopeArgs::default() + }, + path: plugin_dir, + }, + &ServerArgs::default(), + ) + .unwrap(); + + let resolved = resolve_plugins_config(None).unwrap(); + assert_eq!(resolved.dynamic_plugins.len(), 1); + assert_eq!( + resolved.dynamic_plugins[0].plugin_id, + "examples.rust_native_policy" + ); +} + #[test] fn add_registers_dynamic_plugin_in_project_plugins_toml() { let temp = tempfile::tempdir().unwrap(); diff --git a/examples/rust-native-plugin/README.md b/examples/rust-native-plugin/README.md index 69e77d0e..ebc55fd3 100644 --- a/examples/rust-native-plugin/README.md +++ b/examples/rust-native-plugin/README.md @@ -22,9 +22,9 @@ Run this command from the example directory: cargo build ``` -Before you register the plugin, replace `` in -`relay-plugin.toml` with the file name that `cargo build` creates for your -platform: +Before you register the plugin, copy `relay-plugin.toml` to a local manifest and +replace both occurrences of `` with the file name that +`cargo build` creates for your platform: | Platform | Library path | |---|---| @@ -32,12 +32,26 @@ platform: | Linux | `target/debug/libnemo_relay_rust_native_plugin_example.so` | | Windows | `target/debug/nemo_relay_rust_native_plugin_example.dll` | +The copied manifest must use the same relative path for `source.artifact` and +`load.library`. Calculate the library's SHA-256 digest and replace +`` with the lowercase hexadecimal value: + +| Platform | Digest command | +|---|---| +| macOS | `shasum -a 256 ` | +| Linux | `sha256sum ` | +| Windows PowerShell | `(Get-FileHash -Algorithm SHA256).Hash.ToLower()` | + +Keep the `sha256:` prefix in the manifest. For example, a digest of `abc123` +is written as `sha256:abc123`. + ## Register With Relay -After updating `load.library`, run these commands from the repository root: +After materializing the library path and digest, run these commands from the +repository root using the copied manifest path: ```bash -nemo-relay plugins add ./examples/rust-native-plugin/relay-plugin.toml +nemo-relay plugins add ./examples/rust-native-plugin/relay-plugin.local.toml nemo-relay plugins enable examples.rust_native_policy ``` @@ -45,7 +59,7 @@ You can also reference the manifest manually from `plugins.toml`: ```toml [[plugins.dynamic]] -manifest = "./examples/rust-native-plugin/relay-plugin.toml" +manifest = "./examples/rust-native-plugin/relay-plugin.local.toml" [plugins.dynamic.config] tag = "demo" diff --git a/examples/rust-native-plugin/relay-plugin.toml b/examples/rust-native-plugin/relay-plugin.toml index 77644210..27145310 100644 --- a/examples/rust-native-plugin/relay-plugin.toml +++ b/examples/rust-native-plugin/relay-plugin.toml @@ -17,6 +17,14 @@ enabled = false [capabilities] items = ["plugin_native"] +[source] +# Keep this path aligned with `load.library` when materializing the manifest. +artifact = "target/debug/" + +[integrity] +# Replace this placeholder with the SHA-256 digest of `source.artifact`. +sha256 = "sha256:" + [load] # Replace `` with the file built by `cargo build` for # your platform. Refer to README.md for the expected debug artifact names. From cf8ff7d6f68a3f2b2d37fc650a0f8a0216561fd2 Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Mon, 29 Jun 2026 14:54:53 -0600 Subject: [PATCH 2/4] docs: fix native example gateway command Signed-off-by: Bryan Bednarski --- examples/rust-native-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rust-native-plugin/README.md b/examples/rust-native-plugin/README.md index ebc55fd3..82fa142a 100644 --- a/examples/rust-native-plugin/README.md +++ b/examples/rust-native-plugin/README.md @@ -71,7 +71,7 @@ emit_isolated_scope = true Start the gateway normally after the dynamic record is enabled: ```bash -nemo-relay gateway +nemo-relay --bind 127.0.0.1:4040 ``` ## What the Example Registers From a3a450823cef3448ed454833e4a809048958ea29 Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Mon, 29 Jun 2026 15:25:06 -0600 Subject: [PATCH 3/4] test(cli): reject tampered native example artifacts Signed-off-by: Bryan Bednarski --- .../tests/coverage/plugins_lifecycle_tests.rs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/cli/tests/coverage/plugins_lifecycle_tests.rs b/crates/cli/tests/coverage/plugins_lifecycle_tests.rs index 815950f3..00bb5c9e 100644 --- a/crates/cli/tests/coverage/plugins_lifecycle_tests.rs +++ b/crates/cli/tests/coverage/plugins_lifecycle_tests.rs @@ -405,6 +405,52 @@ fn tracked_native_plugin_example_satisfies_default_trust_policy() { ); } +#[test] +fn tracked_native_plugin_example_rejects_tampered_artifact() { + let temp = tempfile::tempdir().unwrap(); + let _env = EnvScope::hermetic(&temp); + let _cwd = CurrentDirGuard::enter(temp.path()); + let plugin_dir = temp.path().join("plugins").join("native-example"); + std::fs::create_dir_all(&plugin_dir).unwrap(); + materialize_native_example_manifest(&plugin_dir); + std::fs::write( + plugin_dir + .join("target") + .join("debug") + .join("libnemo_relay_rust_native_plugin_example.so"), + b"tampered native plugin example fixture", + ) + .unwrap(); + + let error = add( + PluginsAddCommand { + scope: PluginsScopeArgs { + project: true, + ..PluginsScopeArgs::default() + }, + path: plugin_dir, + }, + &ServerArgs::default(), + ) + .unwrap_err(); + + match error { + CliError::PluginLifecycle { + kind: PluginLifecycleFailureKind::Refused, + code: Some("integrity_failed"), + message, + .. + } => assert!(message.contains("failed integrity verification")), + other => panic!("unexpected integrity add error: {other}"), + } + assert!( + resolve_plugins_config(None) + .unwrap() + .dynamic_plugins + .is_empty() + ); +} + #[test] fn add_registers_dynamic_plugin_in_project_plugins_toml() { let temp = tempfile::tempdir().unwrap(); From 1093d8185b5f351c2f1141d06d876443753619b5 Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Mon, 29 Jun 2026 15:37:35 -0600 Subject: [PATCH 4/4] test(cli): use platform native artifact path Signed-off-by: Bryan Bednarski --- .../tests/coverage/plugins_lifecycle_tests.rs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/cli/tests/coverage/plugins_lifecycle_tests.rs b/crates/cli/tests/coverage/plugins_lifecycle_tests.rs index 00bb5c9e..91b31361 100644 --- a/crates/cli/tests/coverage/plugins_lifecycle_tests.rs +++ b/crates/cli/tests/coverage/plugins_lifecycle_tests.rs @@ -351,9 +351,13 @@ symbol = "nemo_relay_fixture_native_plugin" manifest_path } -fn materialize_native_example_manifest(dir: &Path) -> PathBuf { - let artifact_name = "libnemo_relay_rust_native_plugin_example.so"; - let artifact_relative = Path::new("target").join("debug").join(artifact_name); +fn materialize_native_example_manifest(dir: &Path) -> (PathBuf, PathBuf) { + let artifact_name = format!( + "{}nemo_relay_rust_native_plugin_example{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + ); + let artifact_relative = Path::new("target").join("debug").join(&artifact_name); let artifact_path = dir.join(&artifact_relative); std::fs::create_dir_all(artifact_path.parent().unwrap()).unwrap(); let artifact_body = b"native plugin example fixture"; @@ -369,11 +373,11 @@ fn materialize_native_example_manifest(dir: &Path) -> PathBuf { ) .unwrap(); let manifest = template - .replace("", artifact_name) + .replace("", &artifact_name) .replace("", &digest); let manifest_path = dir.join("relay-plugin.toml"); std::fs::write(&manifest_path, manifest).unwrap(); - manifest_path + (manifest_path, artifact_path) } #[test] @@ -412,15 +416,8 @@ fn tracked_native_plugin_example_rejects_tampered_artifact() { let _cwd = CurrentDirGuard::enter(temp.path()); let plugin_dir = temp.path().join("plugins").join("native-example"); std::fs::create_dir_all(&plugin_dir).unwrap(); - materialize_native_example_manifest(&plugin_dir); - std::fs::write( - plugin_dir - .join("target") - .join("debug") - .join("libnemo_relay_rust_native_plugin_example.so"), - b"tampered native plugin example fixture", - ) - .unwrap(); + let (_, artifact_path) = materialize_native_example_manifest(&plugin_dir); + std::fs::write(artifact_path, b"tampered native plugin example fixture").unwrap(); let error = add( PluginsAddCommand {