From 40601764219f84b1625aaa954435d6374b8d9f82 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sat, 25 Jul 2026 18:19:43 -0700 Subject: [PATCH 01/83] feat(plugin): add worker local-model providers Signed-off-by: Alex Fournier --- crates/core/src/lib.rs | 3 + crates/core/src/plugin.rs | 8 + crates/core/src/plugin/dynamic/host.rs | 1 + crates/core/src/plugin/dynamic/worker.rs | 114 ++++++++++- crates/core/src/plugin/local_model.rs | 108 ++++++++++ .../tests/fixtures/worker_plugin/src/main.rs | 60 ++++++ .../tests/integration/worker_plugin_tests.rs | 187 +++++++++++++++++- crates/core/tests/unit/local_model_tests.rs | 70 +++++++ crates/worker-proto/README.md | 5 + .../nemo/relay/worker/v1/plugin_worker.proto | 2 + crates/worker-proto/tests/proto_tests.rs | 1 + crates/worker/README.md | 22 ++- crates/worker/src/lib.rs | 53 +++++ crates/worker/tests/worker_sdk_tests.rs | 55 +++++- .../grpc-worker/grpc-worker-protocol.mdx | 17 +- .../grpc-worker/python/about.mdx | 20 ++ .../grpc-worker/rust/about.mdx | 18 ++ .../worker.py | 7 + .../relay-plugin.toml | 2 +- python/plugin/README.md | 24 ++- .../plugin/src/nemo_relay_plugin/__init__.py | 3 + python/plugin/src/nemo_relay_plugin/_api.py | 37 ++++ .../plugin/test_public_api_docstrings.py | 1 + python/tests/plugin/test_worker_sdk.py | 34 ++++ 24 files changed, 838 insertions(+), 14 deletions(-) create mode 100644 crates/core/src/plugin/local_model.rs create mode 100644 crates/core/tests/unit/local_model_tests.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 18c9f5df1..92e828d52 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -69,6 +69,9 @@ mod registry; pub mod shared_runtime; pub mod stream; +#[cfg(test)] +#[path = "../tests/unit/local_model_tests.rs"] +mod local_model_tests; #[cfg(test)] #[path = "../tests/unit/types_tests.rs"] mod types_tests; diff --git a/crates/core/src/plugin.rs b/crates/core/src/plugin.rs index ff7bc6c79..ae8a7c0c6 100644 --- a/crates/core/src/plugin.rs +++ b/crates/core/src/plugin.rs @@ -48,6 +48,14 @@ pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel}; pub mod dynamic; pub use dynamic::*; +mod local_model; +#[cfg(feature = "worker-grpc")] +pub(crate) use local_model::deregister_local_model_provider_checked; +#[doc(hidden)] +pub use local_model::{ + LocalModelProviderFn, deregister_local_model_provider, local_model_provider, + register_local_model_provider_tracked, +}; type PluginMap = HashMap; diff --git a/crates/core/src/plugin/dynamic/host.rs b/crates/core/src/plugin/dynamic/host.rs index 8b3f3e2df..990becc38 100644 --- a/crates/core/src/plugin/dynamic/host.rs +++ b/crates/core/src/plugin/dynamic/host.rs @@ -300,6 +300,7 @@ impl PluginHostActivation { #[cfg(feature = "worker-grpc")] if let Some(worker) = &mut self.worker { runtime_outcome.merge(worker.deregister_plugin_kinds_checked()); + runtime_outcome.merge(worker.deregister_local_model_providers_checked()); } // A worker cannot be stopped while its registry adapter might still be diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 79e2e70ce..04c1f17db 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -72,8 +72,10 @@ use crate::codec::request::{ANNOTATED_LLM_REQUEST_SCHEMA, AnnotatedLlmRequest}; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::error::{FlowError, Result as FlowResult}; use crate::plugin::{ - ConfigDiagnostic, DiagnosticLevel, Plugin, PluginError, PluginRegistrationContext, - deregister_plugin_registration_checked, register_plugin_tracked, + ConfigDiagnostic, DiagnosticLevel, Plugin, PluginDeregistrationOutcome, PluginError, + PluginRegistrationContext, deregister_local_model_provider_checked, + deregister_plugin_registration_checked, register_local_model_provider_tracked, + register_plugin_tracked, }; use super::{ @@ -129,6 +131,7 @@ pub struct WorkerPluginLoadSpec { pub struct WorkerPluginActivation { plugins: Vec>, plugin_registrations: Vec<(String, u64)>, + local_model_registrations: Vec<(String, u64)>, } impl WorkerPluginActivation { @@ -144,6 +147,12 @@ impl WorkerPluginActivation { deregister_tracked_registrations_checked(&mut self.plugin_registrations, "worker") } + pub(crate) fn deregister_local_model_providers_checked( + &mut self, + ) -> DynamicPluginTeardownOutcome { + deregister_local_model_providers_checked(&mut self.local_model_registrations) + } + pub(crate) fn shutdown_plugins_checked(&self) -> DynamicPluginTeardownOutcome { let mut outcome = DynamicPluginTeardownOutcome::success(); for plugin in self.plugins.iter().rev() { @@ -155,6 +164,7 @@ impl WorkerPluginActivation { impl Drop for WorkerPluginActivation { fn drop(&mut self) { + let _ = deregister_local_model_providers_checked(&mut self.local_model_registrations); for (plugin_kind, registration_id) in self.plugin_registrations.iter().rev() { let _ = deregister_plugin_registration_checked(plugin_kind, *registration_id); } @@ -172,16 +182,23 @@ where let mut activation = WorkerPluginActivation { plugins: Vec::new(), plugin_registrations: Vec::new(), + local_model_registrations: Vec::new(), }; for spec in specs { let instance = load_one_worker_plugin(&spec)?; + let local_model_registrations = instance.install_local_model_providers()?; let plugin_kind = instance.plugin_kind.clone(); + // Transfer ownership before the next fallible registration so Drop can + // unwind providers and the worker process on partial activation. + activation.plugins.push(instance.clone()); + activation + .local_model_registrations + .extend(local_model_registrations); let registration_id = register_plugin_tracked(Arc::new(WorkerPluginAdapter { plugin_kind: plugin_kind.clone(), allows_multiple_components: instance.allows_multiple_components, instance: instance.clone(), }))?; - activation.plugins.push(instance); activation .plugin_registrations .push((plugin_kind, registration_id)); @@ -1043,6 +1060,37 @@ fn clear_host_python_environment(command: &mut Command) { } impl WorkerPluginInstance { + fn install_local_model_providers(&self) -> crate::plugin::Result> { + let mut registrations = Vec::new(); + for registration in &self.registrations { + let surface = RegistrationSurface::try_from(registration.surface).map_err(|_| { + PluginError::RegistrationFailed(format!( + "worker plugin '{}' returned unsupported registration surface {}", + self.plugin_kind, registration.surface + )) + })?; + if surface != RegistrationSurface::LocalModelProvider { + continue; + } + let callback_name = registration.local_name.clone(); + let provider_name = format!("{}/{}", self.plugin_kind, callback_name); + let callback = self.clone_for_callback(); + match register_local_model_provider_tracked( + &provider_name, + Arc::new(move |request, timeout| { + callback.invoke_local_model_provider(&callback_name, request, timeout) + }), + ) { + Ok(registration_id) => registrations.push((provider_name, registration_id)), + Err(error) => { + let _ = deregister_local_model_providers_checked(&mut registrations); + return Err(error); + } + } + } + Ok(registrations) + } + fn install_registrations( &self, ctx: &mut PluginRegistrationContext, @@ -1082,6 +1130,10 @@ impl WorkerPluginInstance { | RegistrationSurface::LlmStreamExecutionIntercept => { self.install_llm_registration(ctx, registration, surface)? } + RegistrationSurface::LocalModelProvider => { + // Providers are installed before static component + // initialization so components can resolve them. + } RegistrationSurface::Unspecified => { return Err(PluginError::RegistrationFailed(format!( "worker plugin '{}' returned unspecified registration surface", @@ -1369,6 +1421,36 @@ struct WorkerPluginCallback { } impl WorkerPluginCallback { + fn invoke_local_model_provider( + &self, + registration_name: &str, + value: Json, + timeout: Duration, + ) -> crate::plugin::Result { + let request = self.base_request( + registration_name, + RegistrationSurface::LocalModelProvider, + None, + Some(invoke_request_payload::Payload::Provider( + json_envelope_infallible(JSON_SCHEMA, &value), + )), + ); + let response = block_on_handle( + &self.runtime, + self.invoke_async_with_timeout(request, timeout), + ) + .map_err(|error| { + PluginError::RegistrationFailed(format!( + "local-model provider '{registration_name}' invocation failed: {error}" + )) + })?; + json_from_invoke_response(response).map_err(|error| { + PluginError::RegistrationFailed(format!( + "local-model provider '{registration_name}' returned an invalid response: {error}" + )) + }) + } + fn log_callback_fallback(&self, callback_name: &str, surface: RegistrationSurface) { log::warn!( target: "nemo_relay.worker", @@ -1381,6 +1463,32 @@ impl WorkerPluginCallback { } } +fn deregister_local_model_providers_checked( + registrations: &mut Vec<(String, u64)>, +) -> DynamicPluginTeardownOutcome { + let mut outcome = DynamicPluginTeardownOutcome::success(); + for (name, registration_id) in std::mem::take(registrations).into_iter().rev() { + match deregister_local_model_provider_checked(&name, registration_id) { + Ok(PluginDeregistrationOutcome::Removed) => {} + Ok(PluginDeregistrationOutcome::Missing) => outcome.record_error( + format!("local-model provider '{name}' was not registered during teardown"), + true, + ), + Ok(PluginDeregistrationOutcome::Replaced) => outcome.record_error( + format!( + "local-model provider '{name}' was replaced during teardown and was left registered" + ), + true, + ), + Err(error) => outcome.record_error( + format!("failed to deregister local-model provider '{name}': {error}"), + false, + ), + } + } + outcome +} + struct WorkerInvocationGuard { runtime: tokio::runtime::Handle, client: PluginWorkerClient, diff --git a/crates/core/src/plugin/local_model.rs b/crates/core/src/plugin/local_model.rs new file mode 100644 index 000000000..117839006 --- /dev/null +++ b/crates/core/src/plugin/local_model.rs @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Process-local model-provider registry used by first-party plugin components. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, LazyLock, RwLock}; +use std::time::Duration; + +use serde_json::Value as Json; + +use super::{PluginDeregistrationOutcome, PluginError, Result}; + +/// JSON request-response provider backed by a local runtime or worker process. +#[doc(hidden)] +pub type LocalModelProviderFn = Arc Result + Send + Sync + 'static>; + +struct RegisteredLocalModelProvider { + registration_id: u64, + callback: LocalModelProviderFn, +} + +static LOCAL_MODEL_PROVIDERS: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); +static NEXT_LOCAL_MODEL_PROVIDER_ID: AtomicU64 = AtomicU64::new(1); + +/// Registers a named local-model provider and returns its ownership token. +#[doc(hidden)] +pub fn register_local_model_provider_tracked( + name: &str, + callback: LocalModelProviderFn, +) -> Result { + let name = name.trim(); + if name.is_empty() { + return Err(PluginError::RegistrationFailed( + "local-model provider name must not be empty".into(), + )); + } + let mut providers = LOCAL_MODEL_PROVIDERS.write().map_err(|error| { + PluginError::Internal(format!( + "local-model provider registry lock poisoned: {error}" + )) + })?; + if providers.contains_key(name) { + return Err(PluginError::RegistrationFailed(format!( + "local-model provider '{name}' is already registered" + ))); + } + let registration_id = NEXT_LOCAL_MODEL_PROVIDER_ID.fetch_add(1, Ordering::Relaxed); + providers.insert( + name.to_string(), + RegisteredLocalModelProvider { + registration_id, + callback, + }, + ); + Ok(registration_id) +} + +/// Resolves a named local-model provider. +#[doc(hidden)] +pub fn local_model_provider(name: &str) -> Result { + let name = name.trim(); + LOCAL_MODEL_PROVIDERS + .read() + .map_err(|error| { + PluginError::Internal(format!( + "local-model provider registry lock poisoned: {error}" + )) + })? + .get(name) + .map(|provider| Arc::clone(&provider.callback)) + .ok_or_else(|| { + PluginError::NotFound(format!("local-model provider '{name}' is not registered")) + }) +} + +/// Deregisters a local-model provider when the ownership token still matches. +#[doc(hidden)] +pub fn deregister_local_model_provider(name: &str, registration_id: u64) -> Result { + deregister_local_model_provider_checked(name, registration_id).map(|outcome| { + matches!( + outcome, + PluginDeregistrationOutcome::Removed | PluginDeregistrationOutcome::Missing + ) + }) +} + +pub(crate) fn deregister_local_model_provider_checked( + name: &str, + registration_id: u64, +) -> Result { + let name = name.trim(); + let mut providers = LOCAL_MODEL_PROVIDERS.write().map_err(|error| { + PluginError::Internal(format!( + "local-model provider registry lock poisoned: {error}" + )) + })?; + match providers.get(name) { + Some(provider) if provider.registration_id == registration_id => { + providers.remove(name); + Ok(PluginDeregistrationOutcome::Removed) + } + Some(_) => Ok(PluginDeregistrationOutcome::Replaced), + None => Ok(PluginDeregistrationOutcome::Missing), + } +} diff --git a/crates/core/tests/fixtures/worker_plugin/src/main.rs b/crates/core/tests/fixtures/worker_plugin/src/main.rs index a9dd41a67..4322a8d08 100644 --- a/crates/core/tests/fixtures/worker_plugin/src/main.rs +++ b/crates/core/tests/fixtures/worker_plugin/src/main.rs @@ -57,6 +57,66 @@ impl WorkerPlugin for FixtureWorkerPlugin { ctx.register_subscriber("", |_| {}); return Ok(()); } + let local_model_provider_names = config + .get("local_model_provider_names") + .and_then(Json::as_array) + .map(|names| { + names + .iter() + .filter_map(Json::as_str) + .map(str::to_string) + .collect::>() + }) + .unwrap_or_else(|| { + vec![ + config + .get("local_model_provider_name") + .and_then(Json::as_str) + .unwrap_or("fixture_local_model") + .to_string(), + ] + }); + for provider_name in local_model_provider_names { + let callback_provider_name = provider_name.clone(); + let exit_in_local_model = fixture_flag(config, "exit_in_local_model"); + ctx.register_local_model_provider(&provider_name, move |request| { + let provider_name = callback_provider_name.clone(); + async move { + if exit_in_local_model { + std::process::exit(45); + } + if let Some(delay_ms) = request.get("delay_ms").and_then(Json::as_u64) { + tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; + } + if let Some(texts) = request.get("texts").and_then(Json::as_array) { + let detections = texts + .iter() + .filter_map(|item| { + let text_id = item.get("id")?.as_u64()?; + let text = item.get("text")?.as_str()?; + let start_utf8 = text.find("PRIVATE")?; + Some(json!({ + "text_id": text_id, + "start_utf8": start_utf8, + "end_utf8": start_utf8 + "PRIVATE".len(), + "label": "fixture_private", + "score": 1.0 + })) + }) + .collect::>(); + return Ok(json!({"version": 1, "detections": detections})); + } + Ok(json!({ + "version": 1, + "request": request, + "provider": provider_name + })) + } + }); + } + if fixture_flag(config, "provider_only") { + return Ok(()); + } let runtime = ctx .runtime() diff --git a/crates/core/tests/integration/worker_plugin_tests.rs b/crates/core/tests/integration/worker_plugin_tests.rs index 4387b048a..7bb6dee2d 100644 --- a/crates/core/tests/integration/worker_plugin_tests.rs +++ b/crates/core/tests/integration/worker_plugin_tests.rs @@ -27,8 +27,9 @@ use nemo_relay::plugin::dynamic::{ WorkerPluginLoadSpec, load_worker_plugins, }; use nemo_relay::plugin::{ - PluginComponentSpec, PluginConfig, clear_plugin_configuration, initialize_plugins_exact, - list_plugin_kinds, + PluginComponentSpec, PluginConfig, clear_plugin_configuration, deregister_local_model_provider, + initialize_plugins_exact, list_plugin_kinds, local_model_provider, + register_local_model_provider_tracked, }; use serde_json::{Map, Value as Json, json}; use sha2::{Digest, Sha256}; @@ -50,6 +51,172 @@ fn worker_activation_with_no_specs_is_empty() { activation.clear(); } +#[tokio::test(flavor = "multi_thread")] +async fn worker_local_model_provider_is_preinstalled_times_out_and_clears() { + let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; + let fixture = build_fixture_worker(); + let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); + let activation = load_worker_plugins([WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::new(), + }]) + .expect("worker plugin should load"); + + // Providers must be available before static consumers initialize. + let provider = local_model_provider("fixture_worker/fixture_local_model") + .expect("worker provider should be installed"); + assert_eq!( + provider( + json!({"text": "private"}), + std::time::Duration::from_secs(1) + ) + .expect("worker provider should return JSON"), + json!({ + "version": 1, + "request": {"text": "private"}, + "provider": "fixture_local_model" + }) + ); + let timeout = provider( + json!({"delay_ms": 100}), + std::time::Duration::from_millis(5), + ) + .expect_err("worker provider should honor the caller deadline") + .to_string(); + assert!(timeout.contains("timed out"), "{timeout}"); + + activation.clear(); + assert!( + local_model_provider("fixture_worker/fixture_local_model").is_err(), + "provider should be removed when the worker activation clears" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn worker_clear_fails_an_in_flight_local_model_call_without_hanging() { + let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; + let fixture = build_fixture_worker(); + let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); + let activation = load_worker_plugins([WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::new(), + }]) + .expect("worker plugin should load"); + let provider = local_model_provider("fixture_worker/fixture_local_model") + .expect("worker provider should be installed"); + let invocation = std::thread::spawn(move || { + provider( + json!({"delay_ms": 5_000}), + std::time::Duration::from_secs(10), + ) + }); + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + activation.clear(); + + let error = invocation + .join() + .expect("provider invocation thread should join") + .expect_err("clearing the worker must fail its in-flight call") + .to_string(); + assert!( + error.contains("invocation failed") || error.contains("cancel"), + "{error}" + ); + assert!( + local_model_provider("fixture_worker/fixture_local_model").is_err(), + "provider should remain deregistered after concurrent clear" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn worker_provider_rolls_back_after_later_plugin_registration_failure() { + let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; + let fixture = build_fixture_worker(); + let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); + let first_provider = "fixture_local_model_first"; + let first_provider_key = format!("fixture_worker/{first_provider}"); + let first = load_worker_plugins([WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([("local_model_provider_name".into(), json!(first_provider))]), + }]) + .expect("first worker plugin should load"); + + let second_provider = "fixture_local_model_rollback"; + let second_provider_key = format!("fixture_worker/{second_provider}"); + let second = load_worker_plugins([WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([("local_model_provider_name".into(), json!(second_provider))]), + }]); + assert!( + second.is_err(), + "duplicate plugin kind should fail after the second provider is installed" + ); + assert!( + local_model_provider(&second_provider_key).is_err(), + "the second provider must be rolled back with its failed activation" + ); + assert!( + local_model_provider(&first_provider_key).is_ok(), + "rollback must not remove the first activation's provider" + ); + + first.clear(); + assert!(local_model_provider(&first_provider_key).is_err()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn worker_provider_rolls_back_earlier_provider_after_same_worker_conflict() { + let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; + let fixture = build_fixture_worker(); + let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); + let first_provider_key = "fixture_worker/fixture_local_model_unique"; + let conflicting_provider_key = "fixture_worker/fixture_local_model_conflict"; + let existing_registration = register_local_model_provider_tracked( + conflicting_provider_key, + Arc::new(|request, _| Ok(json!({"existing": request}))), + ) + .expect("conflicting provider fixture should register"); + + let activation = load_worker_plugins([WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([( + "local_model_provider_names".into(), + json!(["fixture_local_model_unique", "fixture_local_model_conflict"]), + )]), + }]); + + assert!( + activation.is_err(), + "the worker activation should fail on its second provider" + ); + assert!( + local_model_provider(first_provider_key).is_err(), + "an earlier provider from the failed worker must be rolled back" + ); + let existing = local_model_provider(conflicting_provider_key) + .expect("the existing conflicting provider must remain registered"); + assert_eq!( + existing(json!({"value": 1}), std::time::Duration::from_secs(1)) + .expect("existing provider should remain callable"), + json!({"existing": {"value": 1}}) + ); + assert!( + deregister_local_model_provider(conflicting_provider_key, existing_registration) + .expect("existing provider should deregister") + ); +} + #[tokio::test] async fn plugin_host_activation_owns_worker_lifecycle() { let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; @@ -1125,6 +1292,22 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { rewritten["_nemo_relay_plugin"]["tag"], "managed-environment" ); + let local_model = local_model_provider("examples.python_grpc_worker/echo") + .expect("Python worker should expose its local-model provider"); + assert_eq!( + local_model( + json!({"version": 1, "texts": [{"id": 0, "text": "private"}]}), + std::time::Duration::from_secs(1), + ) + .expect("Python local-model provider should round-trip JSON"), + json!({ + "provider": "python_grpc_worker", + "request": { + "version": 1, + "texts": [{"id": 0, "text": "private"}], + }, + }), + ); flush_subscribers().expect("Python callback mark should flush"); find_event( &events.lock().unwrap(), diff --git a/crates/core/tests/unit/local_model_tests.rs b/crates/core/tests/unit/local_model_tests.rs new file mode 100644 index 000000000..871033d92 --- /dev/null +++ b/crates/core/tests/unit/local_model_tests.rs @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; +use std::time::Duration; + +use serde_json::json; + +use crate::plugin::{ + deregister_local_model_provider, local_model_provider, register_local_model_provider_tracked, +}; + +#[test] +fn provider_round_trips_json_and_receives_deadline() { + let registration_id = register_local_model_provider_tracked( + "test-provider", + Arc::new(|request, timeout| { + assert_eq!(timeout, Duration::from_millis(25)); + Ok(json!({"request": request})) + }), + ) + .unwrap(); + + let provider = local_model_provider("test-provider").unwrap(); + assert_eq!( + provider(json!({"text": "hello"}), Duration::from_millis(25)).unwrap(), + json!({"request": {"text": "hello"}}) + ); + assert!(deregister_local_model_provider("test-provider", registration_id).unwrap()); +} + +#[test] +fn ownership_token_does_not_remove_another_registration() { + let registration_id = + register_local_model_provider_tracked("owned-provider", Arc::new(|request, _| Ok(request))) + .unwrap(); + + assert!(!deregister_local_model_provider("owned-provider", registration_id + 1).unwrap()); + assert!(local_model_provider("owned-provider").is_ok()); + assert!(deregister_local_model_provider("owned-provider", registration_id).unwrap()); +} + +#[test] +fn duplicate_provider_names_are_rejected() { + let registration_id = register_local_model_provider_tracked( + "duplicate-provider", + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + let duplicate = register_local_model_provider_tracked( + "duplicate-provider", + Arc::new(|request, _| Ok(request)), + ) + .unwrap_err(); + + assert!(duplicate.to_string().contains("already registered")); + assert!(deregister_local_model_provider("duplicate-provider", registration_id).unwrap()); +} + +#[test] +fn provider_names_are_normalized_consistently() { + let registration_id = register_local_model_provider_tracked( + " normalized-provider ", + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + + assert!(local_model_provider(" normalized-provider ").is_ok()); + assert!(deregister_local_model_provider(" normalized-provider ", registration_id).unwrap()); +} diff --git a/crates/worker-proto/README.md b/crates/worker-proto/README.md index d6e0e21d4..c3d6ca417 100644 --- a/crates/worker-proto/README.md +++ b/crates/worker-proto/README.md @@ -33,6 +33,8 @@ tooling. from `v1` without generating protobuf code in a consumer project. - **Keep data ownership clear**: Carry Relay DTOs in JSON envelopes backed by `nemo-relay-types`; protobuf owns transport control flow. +- **Implement workers in another language**: Generate a `grpc-v1` service from + the protobuf when no maintained language-specific authoring SDK exists. ## What You Get @@ -41,6 +43,9 @@ tooling. clients, servers, services, and messages. - **JSON envelope helpers**: `json_envelope` and `decode_json_envelope` for serializing Relay DTOs into protocol payloads. +- **Language-neutral provider surface**: `LOCAL_MODEL_PROVIDER` carries + component-owned request and response JSON without coupling the host to the + worker implementation language. ## Installation diff --git a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto index 75307e56d..980cddee5 100644 --- a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto +++ b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto @@ -52,6 +52,7 @@ enum RegistrationSurface { MARK_SANITIZE_GUARDRAIL = 30; SCOPE_SANITIZE_START_GUARDRAIL = 31; SCOPE_SANITIZE_END_GUARDRAIL = 32; + LOCAL_MODEL_PROVIDER = 40; } enum LlmCodecKind { @@ -158,6 +159,7 @@ message InvokeRequest { JsonEnvelope event = 10; ToolInvocation tool = 11; LlmInvocation llm = 12; + JsonEnvelope provider = 13; } } diff --git a/crates/worker-proto/tests/proto_tests.rs b/crates/worker-proto/tests/proto_tests.rs index 3b34ad9d1..c623726a3 100644 --- a/crates/worker-proto/tests/proto_tests.rs +++ b/crates/worker-proto/tests/proto_tests.rs @@ -41,6 +41,7 @@ fn registration_surface_values_are_stable() { assert_eq!(RegistrationSurface::MarkSanitizeGuardrail as i32, 30); assert_eq!(RegistrationSurface::ScopeSanitizeStartGuardrail as i32, 31); assert_eq!(RegistrationSurface::ScopeSanitizeEndGuardrail as i32, 32); + assert_eq!(RegistrationSurface::LocalModelProvider as i32, 40); } #[test] diff --git a/crates/worker/README.md b/crates/worker/README.md index 1f5d81ce3..58098f007 100644 --- a/crates/worker/README.md +++ b/crates/worker/README.md @@ -25,7 +25,8 @@ communicates with Relay through the versioned `grpc-v1` worker protocol. - **Isolate plugin code**: Run custom runtime behavior outside the Relay host process. - **Use typed registration APIs**: Implement `WorkerPlugin` and register - subscribers, guardrails, or intercepts with `PluginContext`. + subscribers, guardrails, intercepts, or local-model providers with + `PluginContext`. - **Call the host runtime**: Emit marks, manage scopes, and invoke middleware continuations through `PluginRuntime`. - **Keep lifecycle managed**: Let Relay provide authenticated endpoints and @@ -85,6 +86,25 @@ Relay supplies the socket, activation ID, and authentication token through the worker environment. Use `serve_plugin` for Relay-spawned workers; explicit server configuration is intended for tests and custom launchers. +## Local-Model Providers + +A worker can expose detector or inference functionality to a first-party host +component without owning middleware policy: + +```rust +ctx.register_local_model_provider("detector", |request| async move { + Ok(serde_json::json!({ + "version": 1, + "detections": detect(request)? + })) +}); +``` + +The host publishes the provider as `/detector`. The consuming +component owns the request and response schema, deadline, field selection, +validation, and application of the result. The worker callback should perform +inference only. + ## Concurrency and Cancellation Unary and streaming callbacks run concurrently. Cancellation is cooperative: diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index f2e523f4a..18f2acb95 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -270,6 +270,7 @@ type LlmRequestFn = Arc< type LlmExecutionFn = Arc BoxFutureResult + Send + Sync>; type LlmStreamExecutionFn = Arc BoxFutureResult + Send + Sync>; +type LocalModelProviderFn = Arc BoxFutureResult + Send + Sync>; #[derive(Default)] struct WorkerHandlers { @@ -289,6 +290,7 @@ struct WorkerHandlers { llm_requests: HashMap, llm_executions: HashMap, llm_stream_executions: HashMap, + local_model_providers: HashMap, } /// Registration context passed to [`WorkerPlugin::register`]. @@ -330,6 +332,23 @@ impl PluginContext { .insert(name.into(), Arc::new(callback)); } + /// Registers a named local-model request-response provider. + /// + /// The provider receives and returns versioned JSON data owned by the + /// consuming host component. It does not register middleware or decide + /// which runtime fields are sanitized. + pub fn register_local_model_provider(&mut self, name: &str, callback: F) + where + F: Fn(Json) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + 'static, + { + self.push_registration(name, RegistrationSurface::LocalModelProvider, 0, false); + self.handlers.local_model_providers.insert( + name.into(), + Arc::new(move |request| Box::pin(callback(request))), + ); + } + fn register_event_sanitizer( &mut self, name: &str, @@ -1661,6 +1680,12 @@ impl WorkerService { | RegistrationSurface::LlmExecutionIntercept => { self.invoke_llm_response(request, &scope, surface).await } + RegistrationSurface::LocalModelProvider => { + let payload = provider_payload(request.payload)?; + let handler = self.local_model_provider(&request.registration_name)?; + let future = with_thread_scope(&scope, || handler(payload)); + Ok(json_response(future.await?)) + } RegistrationSurface::LlmStreamExecutionIntercept | RegistrationSurface::Unspecified => { Err(WorkerSdkError::InvalidInput( "surface must use InvokeStream or is unspecified".into(), @@ -2056,6 +2081,20 @@ impl WorkerService { WorkerSdkError::InvalidInput(format!("llm execution '{name}' not registered")) }) } + + fn local_model_provider(&self, name: &str) -> Result { + self.handlers + .lock() + .map_err(|err| WorkerSdkError::Callback(format!("handler lock poisoned: {err}")))? + .local_model_providers + .get(name) + .cloned() + .ok_or_else(|| { + WorkerSdkError::InvalidInput(format!( + "local-model provider '{name}' not registered" + )) + }) + } } struct ToolPayload { @@ -2172,6 +2211,19 @@ fn llm_payload( } } +fn provider_payload( + payload: Option, +) -> Result { + match payload { + Some(nemo_relay_worker_proto::v1::invoke_request::Payload::Provider(value)) => { + decode_json_envelope::(&value).map_err(Into::into) + } + _ => Err(WorkerSdkError::InvalidInput( + "expected local-model provider payload".into(), + )), + } +} + fn required_json( value: Option, field: &str, @@ -2495,6 +2547,7 @@ fn all_surfaces() -> Vec { RegistrationSurface::MarkSanitizeGuardrail, RegistrationSurface::ScopeSanitizeStartGuardrail, RegistrationSurface::ScopeSanitizeEndGuardrail, + RegistrationSurface::LocalModelProvider, ] } diff --git a/crates/worker/tests/worker_sdk_tests.rs b/crates/worker/tests/worker_sdk_tests.rs index 80653155f..7214ff7e1 100644 --- a/crates/worker/tests/worker_sdk_tests.rs +++ b/crates/worker/tests/worker_sdk_tests.rs @@ -114,6 +114,11 @@ async fn worker_service_enforces_auth_and_reports_registrations() { .supported_surfaces .contains(&(RegistrationSurface::LlmStreamExecutionIntercept as i32)) ); + assert!( + handshake + .supported_surfaces + .contains(&(RegistrationSurface::LocalModelProvider as i32)) + ); let bad_health = client .health(Request::new(HealthRequest { @@ -200,7 +205,7 @@ async fn worker_service_enforces_auth_and_reports_registrations() { assert_eq!(invalid_register_config.code(), tonic::Code::InvalidArgument); let registrations = register_plugin(&mut client).await; - assert_eq!(registrations.len(), 21); + assert_eq!(registrations.len(), 22); for local_name in [ "llm-sanitize-request", "llm-sanitize-response", @@ -272,6 +277,32 @@ async fn worker_service_enforces_auth_and_reports_registrations() { handle.abort(); } +#[tokio::test(flavor = "multi_thread")] +async fn worker_service_invokes_local_model_provider() { + let (handle, mut client) = spawn_worker( + Arc::new(SurfacePlugin::default()), + "http://127.0.0.1:9".into(), + ) + .await; + let registrations = register_plugin(&mut client).await; + assert!(registrations.iter().any(|registration| { + registration.local_name == "local-model" + && registration.surface == RegistrationSurface::LocalModelProvider as i32 + })); + + let response = invoke_json( + &mut client, + provider_invoke("local-model", json!({"text": "private"})), + ) + .await; + + assert_eq!( + response, + json!({"text": "private", "provider": "local-model"}) + ); + handle.abort(); +} + #[tokio::test(flavor = "multi_thread")] async fn worker_service_rejects_duplicate_registration_names_on_one_surface() { let (handle, mut client) = spawn_worker( @@ -1226,6 +1257,10 @@ async fn worker_service_reports_missing_handlers_and_malformed_payloads() { ), "llm execution", ), + ( + provider_invoke("missing-local-model", json!({})), + "local-model provider", + ), ] { assert_worker_error( client @@ -1929,6 +1964,9 @@ impl WorkerPlugin for SurfacePlugin { ctx.register_llm_stream_execution_intercept("llm-stream-open-error", 1, |_, _, _| async { Err(WorkerSdkError::Callback("stream open boom".into())) }); + ctx.register_local_model_provider("local-model", |request| async move { + Ok(set_json_field(request, "provider", "local-model")) + }); Ok(()) } } @@ -2521,6 +2559,21 @@ fn tool_invoke( } } +fn provider_invoke(registration_name: &str, value: Json) -> InvokeRequest { + InvokeRequest { + activation_id: ACTIVATION_ID.into(), + invocation_id: "invoke-1".into(), + registration_name: registration_name.into(), + surface: RegistrationSurface::LocalModelProvider as i32, + continuation_id: String::new(), + scope: Some(scope_context()), + auth_token: AUTH_TOKEN.into(), + payload: Some( + nemo_relay_worker_proto::v1::invoke_request::Payload::Provider(json_env(value)), + ), + } +} + fn llm_invoke( registration_name: &str, surface: RegistrationSurface, diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx index b8ca9bfbb..e03f8bfff 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx @@ -22,8 +22,8 @@ Workers implement the `PluginWorker` service: - `Handshake` and `Health` identify a ready worker. - `Validate` returns configuration diagnostics. -- `Register` returns declarative subscriber, guardrail, and intercept - registrations. +- `Register` returns declarative subscriber, guardrail, intercept, and + local-model-provider registrations. - `Invoke` and `InvokeStream` run registered behavior. - `CancelInvocation` requests cancellation, and `Shutdown` requests process termination. @@ -67,12 +67,19 @@ return `WorkerError` without registrations. The supported surfaces are: - `LLM_SANITIZE_REQUEST_GUARDRAIL`, `LLM_SANITIZE_RESPONSE_GUARDRAIL`, `LLM_CONDITIONAL_EXECUTION_GUARDRAIL`, `LLM_REQUEST_INTERCEPT`, `LLM_EXECUTION_INTERCEPT`, and `LLM_STREAM_EXECUTION_INTERCEPT` +- `MARK_SANITIZE_GUARDRAIL`, `SCOPE_SANITIZE_START_GUARDRAIL`, and + `SCOPE_SANITIZE_END_GUARDRAIL` +- `LOCAL_MODEL_PROVIDER` `InvokeRequest` identifies the registration, surface, invocation, optional continuation, and scope context. Its payload is one of an event, tool -invocation, or LLM invocation. `InvokeResponse` returns an empty result, JSON -result, guardrail result, LLM request-intercept result, tool-execution result, -or `WorkerError`. `InvokeStream` emits JSON chunks or `WorkerError` chunks. +invocation, LLM invocation, or component-owned provider request. +`InvokeResponse` returns an empty result, JSON result, guardrail result, LLM +request-intercept result, tool-execution result, or `WorkerError`. +`LOCAL_MODEL_PROVIDER` uses a JSON request and JSON result; the consuming +first-party component owns their versioned schema and publishes the provider +as `/`. `InvokeStream` emits JSON chunks or +`WorkerError` chunks. Every LLM sanitizer invocation includes a directional context with tagged codec identity: `none`, `builtin(id)`, `runtime(id)`, or `opaque`. Worker SDKs expose diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx index 79d20f405..7ae0582e0 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx @@ -165,6 +165,26 @@ registers a tool request intercept that updates the request JSON and emits a mark through `PluginContext.runtime`. The `main` entrypoint blocks until Relay requests shutdown. +### Provide local inference + +Use `register_local_model_provider` when a first-party Relay component owns the +policy and needs an isolated detector or inference implementation: + +```python +async def detect(request: Json) -> Json: + return { + "version": 1, + "detections": await model.detect(request["texts"]), + } + + +ctx.register_local_model_provider("detector", detect) +``` + +Relay publishes this registration as `/detector`. The consuming +component owns the JSON contract, field selection, deadline, response +validation, and application. The worker should perform inference only. + ## Create the Manifest Create `relay-plugin.toml` with the following content: diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx index 9c5fa114f..e2a18653b 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx @@ -68,6 +68,24 @@ identity from `WorkerPlugin::plugin_id()`; custom launchers can use credentials and runs until Relay requests shutdown. Do not start the worker directly for normal operation. +### Provide local inference + +Use `register_local_model_provider` when a first-party Relay component owns the +policy and needs isolated inference: + +```rust +context.register_local_model_provider("detector", |request| async move { + Ok(serde_json::json!({ + "version": 1, + "detections": detect(request)? + })) +}); +``` + +Relay publishes this registration as `/detector`. The consuming +component owns the JSON contract, field selection, deadline, response +validation, and application. The worker should perform inference only. + ## Package the Worker Create `relay-plugin.toml` with the following content. The artifact digest must diff --git a/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py b/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py index fbdfd6a2c..c8b13408f 100644 --- a/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py +++ b/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py @@ -64,7 +64,14 @@ async def tag_tool_request(tool_name: str, args: Json) -> Json: ) return tagged_args + async def echo_local_model(request: Json) -> Json: + return { + "provider": "python_grpc_worker", + "request": request, + } + ctx.register_tool_request_intercept("tag_tool_request", tag_tool_request) + ctx.register_local_model_provider("echo", echo_local_model) def _tag_json(value: Json, tag: str) -> Json: diff --git a/examples/python-grpc-worker-plugin/relay-plugin.toml b/examples/python-grpc-worker-plugin/relay-plugin.toml index c00d9ba80..adc56b525 100644 --- a/examples/python-grpc-worker-plugin/relay-plugin.toml +++ b/examples/python-grpc-worker-plugin/relay-plugin.toml @@ -22,7 +22,7 @@ manifest_root = "." artifact = "nemo_relay_python_grpc_worker_example/worker.py" [integrity] -sha256 = "sha256:966849be254cc6299a17a4bb65500363e9a48f98cc1e0091192e42b23821486f" +sha256 = "sha256:64b20982d6309816947a8de5f9893557fb145c47b4a6fbffc48d506e7f5c695e" [load] runtime = "python" diff --git a/python/plugin/README.md b/python/plugin/README.md index 051383f8d..1fff3c2d7 100644 --- a/python/plugin/README.md +++ b/python/plugin/README.md @@ -26,7 +26,8 @@ protocol. - **Isolate plugin dependencies**: Run custom policy, middleware, or exporter code outside the Relay host process. - **Use the shared runtime contract**: Register subscribers, guardrails, and - intercepts through `WorkerPlugin` and `PluginContext`. + intercepts or local-model providers through `WorkerPlugin` and + `PluginContext`. - **Call back into Relay safely**: Emit marks, create scopes, and continue managed execution through the host runtime handle. - **Keep worker lifecycle managed**: Let Relay provision the worker environment, @@ -104,6 +105,27 @@ worker process. For a complete manifest and runnable plugin, see the [Python gRPC worker plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/python-grpc-worker-plugin/README.md). +## Local-Model Providers + +Use `register_local_model_provider` when a first-party Relay component owns the +policy and needs isolated model inference: + +```python +async def detect(request: Json) -> Json: + return { + "version": 1, + "detections": await model.detect(request["texts"]), + } + + +ctx.register_local_model_provider("detector", detect) +``` + +Relay publishes this provider as `/detector`. The callback may be +synchronous or asynchronous and should perform inference only. The consuming +host component owns the payload schema, deadline, field traversal, output +validation, and result application. + ## Request Intercepts LLM request intercepts return one canonical outcome: diff --git a/python/plugin/src/nemo_relay_plugin/__init__.py b/python/plugin/src/nemo_relay_plugin/__init__.py index 54466a5b5..9ea5a4b2b 100644 --- a/python/plugin/src/nemo_relay_plugin/__init__.py +++ b/python/plugin/src/nemo_relay_plugin/__init__.py @@ -36,6 +36,7 @@ LlmOptimizationTokens: Explicit token evidence by category. LlmOptimizationTokenImpact: Baseline, effective, and saved token evidence. LlmRequestInterceptOutcome: Canonical LLM request-intercept result. + LocalModelProviderCallback: Local-model request-response provider callback. ToolExecutionInterceptOutcome: Canonical tool execution-intercept result. DiagnosticLevel: Severity of a configuration diagnostic. ConfigDiagnostic: Structured configuration warning or error. @@ -96,6 +97,7 @@ LlmSanitizeResponseContext, LlmStreamExecutionCallback, LlmStreamNext, + LocalModelProviderCallback, PendingMarkSpec, PluginContext, PluginRuntime, @@ -142,6 +144,7 @@ "LlmSanitizeResponseCallback", "LlmStreamNext", "LlmStreamExecutionCallback", + "LocalModelProviderCallback", "PluginContext", "PluginRuntime", "PendingMarkSpec", diff --git a/python/plugin/src/nemo_relay_plugin/_api.py b/python/plugin/src/nemo_relay_plugin/_api.py index 7f8b6ccb8..5ad1d044d 100644 --- a/python/plugin/src/nemo_relay_plugin/_api.py +++ b/python/plugin/src/nemo_relay_plugin/_api.py @@ -845,6 +845,7 @@ def register(self, ctx: PluginContext, config: Json) -> None | Awaitable[None]: [str, LlmRequest, "LlmStreamNext"], Iterable[Json] | AsyncIterator[Json] | Awaitable[Iterable[Json] | AsyncIterator[Json]], ] +LocalModelProviderCallback: TypeAlias = Callable[[Json], Json | Awaitable[Json]] @dataclass(slots=True) @@ -865,6 +866,7 @@ class _Handlers: llm_requests: dict[str, LlmRequestCallback] llm_executions: dict[str, LlmExecutionCallback] llm_stream_executions: dict[str, LlmStreamExecutionCallback] + local_model_providers: dict[str, LocalModelProviderCallback] @classmethod def empty(cls) -> _Handlers: @@ -885,6 +887,7 @@ def empty(cls) -> _Handlers: llm_requests={}, llm_executions={}, llm_stream_executions={}, + local_model_providers={}, ) @@ -953,6 +956,26 @@ def register_subscriber(self, name: str, callback: SubscriberCallback) -> None: self._push_registration(name, pb.SUBSCRIBER, 0, False) self._handlers.subscribers[name] = callback + def register_local_model_provider( + self, + name: str, + callback: LocalModelProviderCallback, + ) -> None: + """Register a named local-model request-response provider. + + Args: + name: Stable provider name selected by a consuming host component. + callback: Function receiving and returning component-owned JSON. + The callback can return a value directly or through an + awaitable. + + Provider boundary: + Providers perform model inference only. The consuming host + component owns field selection, policy, and output application. + """ + self._push_registration(name, pb.LOCAL_MODEL_PROVIDER, 0, False) + self._handlers.local_model_providers[name] = callback + def _register_event_sanitizer( self, name: str, @@ -2060,6 +2083,19 @@ async def _invoke_result(self, request: Any) -> Any: ), ) ) + if request.surface == pb.LOCAL_MODEL_PROVIDER: + result = await _maybe_await( + self._handler( + self._handlers.local_model_providers, + request.registration_name, + )( + _decode_required_envelope( + request.provider, + "local-model provider request", + ) + ) + ) + return _json_response(result) raise WorkerSdkError(f"unsupported registration surface {request.surface}") async def _invoke_llm_result(self, request: Any) -> Any: @@ -2195,6 +2231,7 @@ def _all_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, + pb.LOCAL_MODEL_PROVIDER, ] diff --git a/python/tests/plugin/test_public_api_docstrings.py b/python/tests/plugin/test_public_api_docstrings.py index 31abf7427..aa78a349a 100644 --- a/python/tests/plugin/test_public_api_docstrings.py +++ b/python/tests/plugin/test_public_api_docstrings.py @@ -35,6 +35,7 @@ "LlmRequestCallback", "LlmExecutionCallback", "LlmStreamExecutionCallback", + "LocalModelProviderCallback", } diff --git a/python/tests/plugin/test_worker_sdk.py b/python/tests/plugin/test_worker_sdk.py index 7ab918d20..fcb2c146d 100644 --- a/python/tests/plugin/test_worker_sdk.py +++ b/python/tests/plugin/test_worker_sdk.py @@ -363,6 +363,9 @@ async def llm_stream_execution(name: str, request: Json, next_call: Any) -> Asyn async for chunk in stream: yield _tag(chunk, "llm_stream_execution") + async def local_model_provider(request: Json) -> Json: + return _tag(request, "local_model") + ctx.register_subscriber("subscriber", subscriber) ctx.register_mark_sanitize_guardrail("event_sanitize", mark_sanitize, priority=1) ctx.register_scope_sanitize_start_guardrail("event_sanitize", scope_start_sanitize, priority=2) @@ -378,6 +381,7 @@ async def llm_stream_execution(name: str, request: Json, next_call: Any) -> Asyn ctx.register_llm_request_intercept("llm_request", llm_request, priority=9, break_chain=True) ctx.register_llm_execution_intercept("llm_execution", llm_execution, priority=10) ctx.register_llm_stream_execution_intercept("llm_stream_execution", llm_stream_execution, priority=11) + ctx.register_local_model_provider("local_model", local_model_provider) @pytest.fixture(name="host_stub") @@ -411,6 +415,8 @@ def test_generated_proto_matches_worker_contract(): assert pb.MARK_SANITIZE_GUARDRAIL == 30 assert pb.SCOPE_SANITIZE_START_GUARDRAIL == 31 assert pb.SCOPE_SANITIZE_END_GUARDRAIL == 32 + assert pb.LOCAL_MODEL_PROVIDER == 40 + assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["provider"].number == 13 assert pb.CUSTOM == 10 @@ -463,6 +469,7 @@ async def test_health_handshake_validate_register_and_all_surfaces(service: _Wor ("llm_request", pb.LLM_REQUEST_INTERCEPT, 9, True), ("llm_execution", pb.LLM_EXECUTION_INTERCEPT, 10, False), ("llm_stream_execution", pb.LLM_STREAM_EXECUTION_INTERCEPT, 11, False), + ("local_model", pb.LOCAL_MODEL_PROVIDER, 0, False), ] @@ -1438,6 +1445,16 @@ async def test_unary_invoke_success_paths(service: _WorkerService, host_stub: Re assert llm_execution["tag"] == "llm_execution" assert llm_execution["next_llm"]["content"]["llm_execute_gpt-test"] + local_model = await service.Invoke( + _provider_request("local_model", {"text": "private"}), + AbortContext(), + ) + assert local_model.WhichOneof("result") == "json" + assert _envelope_value(local_model.json.value) == { + "text": "private", + "tag": "local_model", + } + async def test_unary_invoke_failure_paths(service: _WorkerService): await _register(service) @@ -1452,6 +1469,13 @@ async def test_unary_invoke_failure_paths(service: _WorkerService): assert missing_handler.WhichOneof("result") == "error" assert "not registered" in missing_handler.error.message + missing_provider = await service.Invoke( + _provider_request("missing", {}), + AbortContext(), + ) + assert missing_provider.WhichOneof("result") == "error" + assert "not registered" in missing_provider.error.message + unsupported = await service.Invoke( _tool_request("tool_request", pb.REGISTRATION_SURFACE_UNSPECIFIED, {}), AbortContext(), @@ -2738,6 +2762,15 @@ def _tool_request(registration_name: str, surface: int, value: Json) -> Any: ) +def _provider_request(registration_name: str, value: Json) -> Any: + return _invoke_request( + registration_name, + pb.LOCAL_MODEL_PROVIDER, + continuation_id="", + provider=_json_envelope(JSON_SCHEMA, value), + ) + + def _llm_payload( *, model_name: str = "model", @@ -2826,4 +2859,5 @@ def _all_expected_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, + pb.LOCAL_MODEL_PROVIDER, ] From 66524dc425c2e8c75c7fe0ab6dd7d4c54f28e60d Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sat, 25 Jul 2026 18:20:04 -0700 Subject: [PATCH 02/83] feat(pii): consume worker-backed local models Signed-off-by: Alex Fournier --- crates/node/pii_redaction.d.ts | 18 +- crates/node/pii_redaction.js | 18 +- crates/node/tests/pii_redaction_tests.mjs | 28 + crates/pii-redaction/Cargo.toml | 2 +- crates/pii-redaction/README.md | 156 +++- crates/pii-redaction/src/builtin.rs | 4 +- crates/pii-redaction/src/component.rs | 132 ++- crates/pii-redaction/src/local.rs | 880 ++++++++++++++++-- .../tests/unit/component_tests.rs | 306 +++++- .../pii-redaction/tests/unit/local_tests.rs | 798 ++++++++++++++++ docs/about-nemo-relay/release-notes/index.mdx | 6 +- .../configure-plugins/pii-redaction/about.mdx | 29 +- .../pii-redaction/configuration.mdx | 150 ++- go/nemo_relay/pii_redaction.go | 57 +- go/nemo_relay/pii_redaction/pii_redaction.go | 10 +- .../pii_redaction/pii_redaction_test.go | 16 +- go/nemo_relay/pii_redaction_test.go | 36 +- justfile | 2 + python/nemo_relay/pii_redaction.py | 48 +- python/nemo_relay/pii_redaction.pyi | 15 + python/tests/test_pii_redaction_plugin.py | 50 + 21 files changed, 2539 insertions(+), 222 deletions(-) create mode 100644 crates/pii-redaction/tests/unit/local_tests.rs diff --git a/crates/node/pii_redaction.d.ts b/crates/node/pii_redaction.d.ts index 5a5f20e76..2df3407ea 100644 --- a/crates/node/pii_redaction.d.ts +++ b/crates/node/pii_redaction.d.ts @@ -26,10 +26,23 @@ export interface LocalModelConfig { backend?: string; model_id?: string; detector_profile?: string; + target_paths?: string[]; + target_path_patterns?: string[]; + min_score?: number; + excluded_labels?: string[]; + replacement?: string; allow_network?: boolean; max_latency_ms?: number; } +export interface ProfileConfig { + enabled?: boolean; + mode?: 'builtin' | 'local_model' | string; + priority?: number; + builtin?: BuiltinConfig; + local?: LocalModelConfig; +} + export interface Config { version?: number; mode?: 'builtin' | 'local_model' | string; @@ -40,6 +53,7 @@ export interface Config { mark?: boolean; priority?: number; codec?: 'openai_chat' | 'openai_responses' | 'anthropic_messages' | string; + profiles?: ProfileConfig[]; builtin?: BuiltinConfig; local?: LocalModelConfig; policy?: ConfigPolicy; @@ -57,8 +71,10 @@ export declare const PII_REDACTION_PLUGIN_KIND: 'pii_redaction'; export declare function defaultConfig(): Config; /** Create deterministic built-in redaction backend settings with defaults applied. */ export declare function builtinConfig(config?: BuiltinConfig): BuiltinConfig; -/** Create future local-model backend settings with defaults applied. */ +/** Create worker-backed local-model provider settings. */ export declare function localModelConfig(config?: LocalModelConfig): LocalModelConfig; +/** Create one ordered redaction profile with defaults applied. */ +export declare function profileConfig(config?: ProfileConfig): ProfileConfig; /** Wrap PII redaction config as a top-level plugin component. */ export declare function ComponentSpec( config: Config, diff --git a/crates/node/pii_redaction.js b/crates/node/pii_redaction.js index c5fc6d313..dea68d566 100644 --- a/crates/node/pii_redaction.js +++ b/crates/node/pii_redaction.js @@ -39,7 +39,7 @@ function builtinConfig(config = {}) { } /** - * Create future local-model backend settings with defaults applied. + * Create worker-backed local-model redaction settings. * * @param {object} [config={}] - Partial local-model settings to override. * @returns {object} A normalized local-model backend config object. @@ -50,6 +50,21 @@ function localModelConfig(config = {}) { }; } +/** + * Create one ordered redaction profile with defaults applied. + * + * @param {object} [config={}] - Partial profile settings to override. + * @returns {object} A normalized redaction profile object. + */ +function profileConfig(config = {}) { + return { + enabled: true, + mode: 'builtin', + priority: 100, + ...config, + }; +} + /** * Wrap PII redaction config as a top-level plugin component. * @@ -68,5 +83,6 @@ module.exports = { defaultConfig, builtinConfig, localModelConfig, + profileConfig, ComponentSpec, }; diff --git a/crates/node/tests/pii_redaction_tests.mjs b/crates/node/tests/pii_redaction_tests.mjs index 0c0edc3f7..ad529e292 100644 --- a/crates/node/tests/pii_redaction_tests.mjs +++ b/crates/node/tests/pii_redaction_tests.mjs @@ -23,6 +23,11 @@ describe('pii_redaction plugin helpers', () => { }); assert.deepEqual(piiRedaction.builtinConfig(), { action: 'remove' }); assert.deepEqual(piiRedaction.localModelConfig(), {}); + assert.deepEqual(piiRedaction.profileConfig(), { + enabled: true, + mode: 'builtin', + priority: 100, + }); const component = piiRedaction.ComponentSpec({ ...piiRedaction.defaultConfig(), @@ -32,6 +37,29 @@ describe('pii_redaction plugin helpers', () => { assert.equal(component.enabled, true); }); + it('builds profile composition without legacy top-level fields', () => { + const config = { + version: 1, + codec: 'openai_chat', + profiles: [ + piiRedaction.profileConfig({ + builtin: piiRedaction.builtinConfig({ detector: 'email' }), + }), + piiRedaction.profileConfig({ + mode: 'local_model', + priority: 110, + local: piiRedaction.localModelConfig({ + backend: 'acme.pii/detector', + target_paths: ['/message'], + }), + }), + ], + }; + + assert.equal(config.mode, undefined); + assert.deepEqual(plugin.validate({ version: 1, components: [piiRedaction.ComponentSpec(config)] }).diagnostics, []); + }); + it('lists builtin pii_redaction kind and validates bad values', () => { assert.equal(plugin.listKinds().includes(piiRedaction.PII_REDACTION_PLUGIN_KIND), true); const report = plugin.validate({ diff --git a/crates/pii-redaction/Cargo.toml b/crates/pii-redaction/Cargo.toml index 006a45b59..fb2163f23 100644 --- a/crates/pii-redaction/Cargo.toml +++ b/crates/pii-redaction/Cargo.toml @@ -27,7 +27,7 @@ sha2 = "0.11" schemars = { version = "0.8", optional = true } [dev-dependencies] -nemo-relay = { workspace = true, features = ["openinference", "otel"] } +nemo-relay = { workspace = true, features = ["openinference", "otel", "worker-grpc"] } futures = "0.3" tokio = { version = "1", features = ["rt", "macros", "sync", "test-util", "rt-multi-thread", "time"] } opentelemetry_sdk = { workspace = true, features = ["trace", "testing"] } diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index 4c8528582..3ef479846 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0 `nemo-relay-pii-redaction` is the first-party NeMo Relay plugin crate for deterministic privacy redaction on tool and LLM observability payloads. It ships the `pii_redaction` plugin contract, a production-ready `builtin` -backend, and the future `local_model` seam for model-backed detection and +backend, and a worker-backed `local_model` seam for model-backed detection and redaction. The plugin is designed for the common case where teams want a supported, @@ -36,8 +36,8 @@ NeMo Relay PII Redaction allows you to: `openai_responses`, and `anthropic_messages`. - Remove conversational trajectory content while preserving event structure, tool-call identity, model attribution, routing, usage, and cost analytics. -- Use the `local_model` config contract and provider registration surface for - future model-backed implementations. +- Use an isolated `grpc-v1` worker as the detector behind the `local_model` + config contract without loading model dependencies into the Relay host. ## Plugin Versus Raw Middleware @@ -188,13 +188,151 @@ high-risk secrets, prefer `redact` over partial `mask` behavior. ## Local Model Seam -`local_model` is included in the plugin contract now, but no runtime -implementation ships in this crate yet. +`local_model` delegates bounded detector inference to a manifest-backed +`grpc-v1` worker. The PII component remains responsible for choosing +observability fields, decoding provider payloads, batching text, enforcing the +deadline and failure policy, validating detections, and replacing accepted +spans. -The seam exists so a future local detector/redactor backend can be added -without redesigning the public plugin surface. If `mode = "local_model"` is -configured today, the runtime expects a registered local backend provider and -fails fast if one is not installed. +Configure the provider by its host-qualified name: + +```toml +[[components]] +kind = "pii_redaction" +enabled = true + +[components.config] +mode = "local_model" +codec = "openai_chat" + +[components.config.local] +backend = "acme.pii_worker/detector" +model_id = "acme-pii-v1" +detector_profile = "default" +min_score = 0.4 +target_path_patterns = [ + "/messages/*/content", + "/messages/*/content/*/text", + "/message", + "/message/*/text", +] +replacement = "[REDACTED]" +allow_network = false +max_latency_ms = 250 +``` + +The backend name is `/`. For example, a worker +with plugin ID `acme.pii_worker` that calls +`register_local_model_provider("detector", ...)` is selected as +`acme.pii_worker/detector`. Relay installs worker providers before static +components initialize and removes PII sanitizers before stopping their worker. + +Use profiles to compose deterministic and contextual detection. The lower +priority runs first: + +```toml +[components.config] +codec = "openai_chat" + +[[components.config.profiles]] +mode = "builtin" +priority = 80 + +[components.config.profiles.builtin] +action = "redact" +detector = "email" + +[[components.config.profiles]] +mode = "local_model" +priority = 90 + +[components.config.profiles.local] +backend = "nemo_relay.pii_rampart/detector" +min_score = 0.4 +max_latency_ms = 1500 +target_path_patterns = [ + "/messages/*/content", + "/messages/*/content/*/text", + "/message", + "/message/*/text", +] +``` + +`target_paths` contains exact JSON pointers. `target_path_patterns` also accepts +`*` as one complete path segment, which is useful for message arrays. When a +codec is configured, paths address the normalized request or response shape; +the content-only patterns above cover the `openai_chat` request and response +shape. Without a codec, they address the original JSON payload. When both lists +are empty, Relay inspects every string leaf and reports a configuration warning. + +Use content-only paths for contextual classifiers. Do not send model names, +tool identifiers, trace IDs, routing fields, or arbitrary provider metadata to +a classifier unless that is an explicit policy choice. + +Relay accepts detections whose confidence is at least `min_score`, which +defaults to `0.4`. `excluded_labels` is an exact, case-sensitive denylist for +provider labels that should remain visible. The host applies both settings +after validating the complete provider response; workers do not own the final +redaction policy. + +Provider failures, timeouts, malformed responses, invalid UTF-8 boundaries, +overlapping spans, and input-limit violations fail closed for the affected +batch. If a configured codec cannot decode or safely re-encode an LLM payload, +Relay replaces the entire emitted request or response body; it does not retry +normalized selectors against the raw provider shape. `allow_network = true` is +rejected; this lane is for same-machine inference. This setting is a +configuration invariant, not a network sandbox: Relay's worker launcher does +not currently prevent a worker process from opening sockets. Only install +providers whose packaging and runtime behavior satisfy that policy. The +default deadline is 250 ms for the complete selected payload, including every +provider batch. Configuration above 60 seconds is rejected. + +### Provider Contract + +The worker receives a versioned JSON request: + +```json +{ + "version": 1, + "model_id": "acme-pii-v1", + "detector_profile": "default", + "texts": [ + {"id": 0, "text": "Contact Alice Rivera"} + ] +} +``` + +It returns detections using UTF-8 byte offsets: + +```json +{ + "version": 1, + "detections": [ + { + "text_id": 0, + "start_utf8": 8, + "end_utf8": 20, + "label": "person", + "score": 0.99 + } + ] +} +``` + +The provider performs inference only. It must not choose Relay surfaces, +traverse arbitrary event fields, or apply replacements itself. Rust and Python +workers have SDK helpers for this registration. Other languages can implement +the same `grpc-v1` protobuf contract directly; Rust, Python, and Node hosts all +consume it through the shared core runtime. + +### Optional Rampart Provider + +The source tree includes an optional +[Rampart worker](./providers/rampart/README.md) that implements this provider +contract with a pinned ONNX token-classification model. It runs in a +Relay-managed Python worker process, keeps ONNX dependencies out of the host, +and complements the built-in deterministic recognizers. The model is acquired +at activation time and is not distributed in the Relay package. ## Documentation diff --git a/crates/pii-redaction/src/builtin.rs b/crates/pii-redaction/src/builtin.rs index 6e6d1dc5c..1fac4b952 100644 --- a/crates/pii-redaction/src/builtin.rs +++ b/crates/pii-redaction/src/builtin.rs @@ -696,7 +696,7 @@ fn remove_sanitized_json_pointer_value(value: &mut Json, segments: &[String]) -> } } -fn render_json_pointer_path(path_segments: &[String]) -> String { +pub(super) fn render_json_pointer_path(path_segments: &[String]) -> String { if path_segments.is_empty() { return String::new(); } @@ -708,7 +708,7 @@ fn render_json_pointer_path(path_segments: &[String]) -> String { rendered } -fn escape_json_pointer_segment(segment: &str) -> String { +pub(super) fn escape_json_pointer_segment(segment: &str) -> String { segment.replace('~', "~0").replace('/', "~1") } diff --git a/crates/pii-redaction/src/component.rs b/crates/pii-redaction/src/component.rs index 2f6b9bfd3..8f574b507 100644 --- a/crates/pii-redaction/src/component.rs +++ b/crates/pii-redaction/src/component.rs @@ -24,11 +24,19 @@ use super::builtin::{ #[cfg(test)] pub(crate) use super::builtin::{hex_sha256, mask_text}; use super::detectors::{detector_regex_pattern, supported_detector_summary}; -use super::local::register_local_backend; -pub use super::local::{clear_local_backend_provider, register_local_backend_provider}; +use super::local::{register_local_backend, validate_local_backend_config}; /// The plugin kind reserved for the built-in privacy component. pub const PII_REDACTION_PLUGIN_KIND: &str = "pii_redaction"; +pub(super) const DEFAULT_LOCAL_MODEL_LATENCY_MS: u64 = 250; +pub(super) const DEFAULT_LOCAL_MODEL_MIN_SCORE: f64 = 0.4; +pub(super) const MAX_LOCAL_MODEL_LATENCY_MS: u64 = 60_000; +pub(super) const MAX_LOCAL_MODEL_TARGET_PATHS: usize = 256; +pub(super) const MAX_LOCAL_MODEL_TARGET_PATH_BYTES: usize = 1024; +pub(super) const MAX_LOCAL_MODEL_REPLACEMENT_BYTES: usize = 1024; +pub(super) const MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES: usize = 1024; +pub(super) const MAX_LOCAL_MODEL_EXCLUDED_LABELS: usize = 128; +pub(super) const MAX_LOCAL_MODEL_LABEL_BYTES: usize = 128; /// Top-level PII redaction component wrapper. #[derive(Debug, Clone)] @@ -237,23 +245,38 @@ impl Default for BuiltinBackendConfig { } } -/// Local-backend settings for a future in-process local-model runtime. +/// Local-backend settings for a same-machine local-model provider. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct LocalBackendConfig { - /// Optional local-model backend identifier. + /// Registered local-model provider identifier. #[serde(default, skip_serializing_if = "Option::is_none")] pub backend: Option, - /// Optional model identifier reserved for future local-model runtimes. + /// Optional model identifier passed to the provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub model_id: Option, - /// Optional detector profile reserved for future local-model runtimes. + /// Optional detector profile passed to the provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub detector_profile: Option, - /// Whether a future local-model backend may use network calls. + /// Exact JSON-pointer paths to inspect. Empty means every string leaf. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub target_paths: Vec, + /// JSON-pointer patterns to inspect. A `*` segment matches one path segment. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub target_path_patterns: Vec, + /// Minimum provider confidence accepted for redaction. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_score: Option, + /// Provider labels that should not be redacted. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub excluded_labels: Vec, + /// Replacement applied to every accepted detection. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub replacement: Option, + /// Whether the provider may use network calls. #[serde(default, skip_serializing_if = "Option::is_none")] pub allow_network: Option, - /// Target latency budget hint for a future local-model backend. + /// Per-batch provider deadline in milliseconds. #[serde(default, skip_serializing_if = "Option::is_none")] pub max_latency_ms: Option, } @@ -398,6 +421,11 @@ nemo_relay::editor_config! { backend => { label: "backend", kind: String, optional: true }, model_id => { label: "model_id", kind: String, optional: true }, detector_profile => { label: "detector_profile", kind: String, optional: true }, + target_paths => { label: "target_paths", kind: List, list: &nemo_relay::config_editor::STRING_LIST_ITEM }, + target_path_patterns => { label: "target_path_patterns", kind: List, list: &nemo_relay::config_editor::STRING_LIST_ITEM }, + min_score => { label: "min_score", kind: Float, optional: true }, + excluded_labels => { label: "excluded_labels", kind: List, list: &nemo_relay::config_editor::STRING_LIST_ITEM }, + replacement => { label: "replacement", kind: String, optional: true }, allow_network => { label: "allow_network", kind: Boolean, optional: true }, max_latency_ms => { label: "max_latency_ms", kind: Integer, optional: true }, } @@ -672,6 +700,11 @@ fn validate_pii_redaction_plugin_config_with_policy( "backend", "model_id", "detector_profile", + "target_paths", + "target_path_patterns", + "min_score", + "excluded_labels", + "replacement", "allow_network", "max_latency_ms", ], @@ -683,6 +716,7 @@ fn validate_pii_redaction_plugin_config_with_policy( validate_builtin_mode_requirements(&mut diagnostics, &config.policy, plugin_config, &config); validate_builtin_action_requirements(&mut diagnostics, &config.policy, plugin_config, &config); validate_local_mode_requirements(&mut diagnostics, &config.policy, plugin_config, &config); + validate_local_backend_requirements(&mut diagnostics, &config.policy, &config); diagnostics } @@ -774,6 +808,11 @@ fn validate_profile_configuration( "backend", "model_id", "detector_profile", + "target_paths", + "target_path_patterns", + "min_score", + "excluded_labels", + "replacement", "allow_network", "max_latency_ms", ], @@ -797,16 +836,11 @@ fn validate_profile_configuration( raw_profile, &profile_config, ); - if profile.mode == "local_model" && !raw_profile.contains_key("local") { - push_policy_diag( - &mut profile_diagnostics, - config.policy.unsupported_value, - "pii_redaction.unsupported_value", - Some(PII_REDACTION_PLUGIN_KIND.to_string()), - Some("local".to_string()), - "`local` settings are required for a local-model profile".to_string(), - ); - } + validate_local_backend_requirements( + &mut profile_diagnostics, + &config.policy, + &profile_config, + ); prefix_profile_diagnostics(&mut profile_diagnostics, index); diagnostics.extend(profile_diagnostics); } @@ -867,6 +901,16 @@ fn validate_local_mode_requirements( config: &PiiRedactionConfig, ) { if config.mode == "local_model" { + if !plugin_config.contains_key("local") { + push_policy_diag( + diagnostics, + policy.unsupported_value, + "pii_redaction.unsupported_value", + Some(PII_REDACTION_PLUGIN_KIND.to_string()), + Some("local".to_string()), + "`local` settings are required when mode = 'local_model'".to_string(), + ); + } return; } if !plugin_config.contains_key("local") { @@ -883,6 +927,58 @@ fn validate_local_mode_requirements( ); } +fn validate_local_backend_requirements( + diagnostics: &mut Vec, + policy: &ConfigPolicy, + config: &PiiRedactionConfig, +) { + if config.mode != "local_model" { + return; + } + let Some(local) = config.local.as_ref() else { + return; + }; + for violation in validate_local_backend_config(local) { + push_policy_diag( + diagnostics, + policy.unsupported_value, + "pii_redaction.unsupported_value", + Some(PII_REDACTION_PLUGIN_KIND.to_string()), + Some(violation.field.to_string()), + violation.message, + ); + } + if local.target_paths.is_empty() && local.target_path_patterns.is_empty() { + diagnostics.push(ConfigDiagnostic { + level: DiagnosticLevel::Warning, + code: "pii_redaction.local_model_all_paths".to_string(), + component: Some(PII_REDACTION_PLUGIN_KIND.to_string()), + field: Some("local.target_paths".to_string()), + message: "local-model PII redaction has no target paths and will inspect every string leaf; configure explicit content paths to avoid classifying identifiers and metadata".to_string(), + }); + } +} + +pub(super) fn is_valid_json_pointer(path: &str) -> bool { + if path.is_empty() { + return true; + } + if !path.starts_with('/') { + return false; + } + let mut bytes = path.as_bytes().iter().copied(); + while let Some(byte) = bytes.next() { + if byte == b'~' && !matches!(bytes.next(), Some(b'0' | b'1')) { + return false; + } + } + true +} + +pub(super) fn is_valid_json_pointer_pattern(path: &str) -> bool { + is_valid_json_pointer(path) +} + fn validate_builtin_mode_requirements( diagnostics: &mut Vec, policy: &ConfigPolicy, diff --git a/crates/pii-redaction/src/local.rs b/crates/pii-redaction/src/local.rs index b1198c43b..23d1f0651 100644 --- a/crates/pii-redaction/src/local.rs +++ b/crates/pii-redaction/src/local.rs @@ -1,115 +1,819 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use std::sync::{Arc, LazyLock, Mutex, MutexGuard}; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use nemo_relay::api::event::{CategoryProfile, Event}; +use nemo_relay::api::llm::LlmRequest; +use nemo_relay::api::runtime::{ + EventSanitizeFn, LlmSanitizeRequestFn, LlmSanitizeResponseFn, ToolSanitizeFn, +}; +use nemo_relay::codec::resolve::{ + ProviderSurface, request_codec as build_request_codec, response_codec as build_response_codec, +}; +use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_relay::plugin::{ - PluginError, PluginRegistrationContext, Result as PluginResult, rollback_registrations, + LocalModelProviderFn, PluginError, PluginRegistrationContext, Result as PluginResult, + local_model_provider, }; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; -use super::component::PiiRedactionConfig; -use super::component::profile_registration_prefix; +use super::component::{ + DEFAULT_LOCAL_MODEL_LATENCY_MS, DEFAULT_LOCAL_MODEL_MIN_SCORE, LocalBackendConfig, + MAX_LOCAL_MODEL_EXCLUDED_LABELS, MAX_LOCAL_MODEL_LABEL_BYTES, MAX_LOCAL_MODEL_LATENCY_MS, + MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES, MAX_LOCAL_MODEL_REPLACEMENT_BYTES, + MAX_LOCAL_MODEL_TARGET_PATH_BYTES, MAX_LOCAL_MODEL_TARGET_PATHS, PiiRedactionConfig, + is_valid_json_pointer, is_valid_json_pointer_pattern, profile_registration_prefix, +}; +use super::overlay::BuiltinCodecName; -#[doc(hidden)] -pub type LocalBackendProvider = Arc< - dyn Fn(PiiRedactionConfig, &mut PluginRegistrationContext) -> PluginResult<()> + Send + Sync, ->; +const LOCAL_MODEL_CONTRACT_VERSION: u32 = 1; +const MAX_BATCH_ITEMS: usize = 64; +const MAX_BATCH_BYTES: usize = 64 * 1024; +const MAX_TEXT_BYTES: usize = 16 * 1024; +const MAX_TEXTS_PER_PAYLOAD: usize = 256; +const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; +const MAX_DETECTIONS_PER_TEXT: usize = 128; -static LOCAL_BACKEND_PROVIDER: LazyLock>> = - LazyLock::new(|| Mutex::new(None)); +#[derive(Clone)] +struct CompiledLocalBackend { + provider_name: Arc, + provider: LocalModelProviderFn, + model_id: Option, + detector_profile: Option, + target_paths: Arc>, + target_path_patterns: Arc>, + min_score: f64, + excluded_labels: Arc>, + replacement: Arc, + timeout: Duration, + request_codec: Option>, + response_codec: Option>, + codec_name: Option, +} -fn local_backend_provider_guard() -> PluginResult>> -{ - LOCAL_BACKEND_PROVIDER.lock().map_err(|e| { - PluginError::Internal(format!( - "PII redaction local backend provider lock poisoned: {e}" - )) - }) +#[derive(Clone)] +struct JsonPointerPattern { + segments: Vec, } -#[doc(hidden)] -pub fn register_local_backend_provider(provider: LocalBackendProvider) -> PluginResult<()> { - let mut guard = local_backend_provider_guard()?; - *guard = Some(provider); - Ok(()) +impl JsonPointerPattern { + fn compile(pattern: String) -> Self { + let segments = pattern.strip_prefix('/').map_or_else(Vec::new, |path| { + path.split('/').map(str::to_string).collect() + }); + Self { segments } + } + + fn matches(&self, path: &[String]) -> bool { + self.segments.len() == path.len() + && self + .segments + .iter() + .zip(path) + .all(|(pattern, segment)| pattern == "*" || pattern == segment) + } } -#[doc(hidden)] -pub fn clear_local_backend_provider() -> PluginResult<()> { - let mut guard = local_backend_provider_guard()?; - *guard = None; - Ok(()) +#[derive(Serialize)] +struct LocalModelRequest<'a> { + version: u32, + #[serde(skip_serializing_if = "Option::is_none")] + model_id: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + detector_profile: Option<&'a str>, + texts: Vec>, } -pub(super) fn register_local_backend( - config: PiiRedactionConfig, - ctx: &mut PluginRegistrationContext, - profile_name: Option<&str>, -) -> PluginResult<()> { - let provider = local_backend_provider_guard()?.clone(); +#[derive(Serialize)] +struct LocalModelText<'a> { + id: u32, + text: &'a str, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LocalModelResponse { + version: u32, + detections: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LocalModelDetection { + text_id: u32, + start_utf8: usize, + end_utf8: usize, + label: String, + score: f64, +} + +struct SelectedText { + text: String, + eligible: bool, +} + +impl CompiledLocalBackend { + fn new(config: LocalBackendConfig, codec_name: Option) -> PluginResult { + if let Some(violation) = validate_local_backend_config(&config).into_iter().next() { + return Err(PluginError::InvalidConfig(violation.message)); + } + let provider_name = config + .backend + .as_deref() + .map(str::trim) + .expect("validated local backend has a provider name") + .to_string(); + let min_score = config.min_score.unwrap_or(DEFAULT_LOCAL_MODEL_MIN_SCORE); + let replacement = config + .replacement + .unwrap_or_else(|| "[REDACTED]".to_string()); + let max_latency_ms = config + .max_latency_ms + .unwrap_or(DEFAULT_LOCAL_MODEL_LATENCY_MS); + let surface = match codec_name.as_deref() { + Some(name) => Some(ProviderSurface::from_codec_name(name).ok_or_else(|| { + PluginError::InvalidConfig(format!("unsupported codec '{name}'")) + })?), + None => None, + }; + let provider = local_model_provider(&provider_name).map_err(|_| { + PluginError::RegistrationFailed(format!( + "PII redaction local-model provider '{provider_name}' is unavailable" + )) + })?; + Ok(Self { + provider_name: Arc::new(provider_name), + provider, + model_id: config.model_id.map(|value| value.trim().to_string()), + detector_profile: config + .detector_profile + .map(|value| value.trim().to_string()), + target_paths: Arc::new(config.target_paths.into_iter().collect()), + target_path_patterns: Arc::new( + config + .target_path_patterns + .into_iter() + .map(JsonPointerPattern::compile) + .collect(), + ), + min_score, + excluded_labels: Arc::new( + config + .excluded_labels + .into_iter() + .map(|label| label.trim().to_string()) + .collect(), + ), + replacement: Arc::new(replacement), + timeout: Duration::from_millis(max_latency_ms), + request_codec: surface.map(build_request_codec), + response_codec: surface.map(build_response_codec), + codec_name: surface.map(BuiltinCodecName::from_provider_surface), + }) + } + + fn sanitize_json(&self, mut value: Json) -> Json { + let mut texts = Vec::new(); + let mut total_bytes = 0; + let mut within_budget = true; + self.collect_strings( + &value, + &mut Vec::new(), + &mut texts, + &mut total_bytes, + &mut within_budget, + ); + let sanitized = self.sanitize_texts(texts); + let mut index = 0; + self.replace_strings(&mut value, &mut Vec::new(), &sanitized, &mut index); + value + } + + fn collect_strings( + &self, + value: &Json, + path: &mut Vec, + texts: &mut Vec, + total_bytes: &mut usize, + within_budget: &mut bool, + ) { + match value { + Json::String(text) if self.matches_path(path) && *within_budget => { + if texts.len() >= MAX_TEXTS_PER_PAYLOAD { + *within_budget = false; + return; + } + if text.len() > MAX_TEXT_BYTES { + texts.push(SelectedText { + text: self.replacement.as_str().to_string(), + eligible: false, + }); + return; + } + let Some(next_total) = total_bytes.checked_add(text.len()) else { + *within_budget = false; + return; + }; + if next_total > MAX_PAYLOAD_TEXT_BYTES { + *within_budget = false; + return; + } + *total_bytes = next_total; + texts.push(SelectedText { + text: text.clone(), + eligible: true, + }); + } + Json::Array(items) => { + for (index, item) in items.iter().enumerate() { + path.push(index.to_string()); + self.collect_strings(item, path, texts, total_bytes, within_budget); + path.pop(); + } + } + Json::Object(fields) => { + for (key, value) in fields { + path.push(super::builtin::escape_json_pointer_segment(key)); + self.collect_strings(value, path, texts, total_bytes, within_budget); + path.pop(); + } + } + _ => {} + } + } + + fn replace_strings( + &self, + value: &mut Json, + path: &mut Vec, + sanitized: &[String], + index: &mut usize, + ) { + match value { + Json::String(text) if self.matches_path(path) => { + if let Some(replacement) = sanitized.get(*index) { + *text = replacement.clone(); + } else { + *text = self.replacement.as_str().to_string(); + } + *index += 1; + } + Json::Array(items) => { + for (item_index, item) in items.iter_mut().enumerate() { + path.push(item_index.to_string()); + self.replace_strings(item, path, sanitized, index); + path.pop(); + } + } + Json::Object(fields) => { + for (key, value) in fields { + path.push(super::builtin::escape_json_pointer_segment(key)); + self.replace_strings(value, path, sanitized, index); + path.pop(); + } + } + _ => {} + } + } + + fn matches_path(&self, path: &[String]) -> bool { + (self.target_paths.is_empty() && self.target_path_patterns.is_empty()) + || self + .target_paths + .contains(&super::builtin::render_json_pointer_path(path)) + || self + .target_path_patterns + .iter() + .any(|pattern| pattern.matches(path)) + } + + fn sanitize_texts(&self, mut texts: Vec) -> Vec { + let eligible = texts + .iter() + .enumerate() + .filter_map(|(index, text)| text.eligible.then_some(index)) + .collect::>(); + + let mut cursor = 0; + let deadline = Instant::now() + self.timeout; + while cursor < eligible.len() { + let start = cursor; + let mut batch_bytes = 0; + while cursor < eligible.len() && cursor - start < MAX_BATCH_ITEMS { + let next_bytes = texts[eligible[cursor]].text.len(); + if cursor > start && batch_bytes + next_bytes > MAX_BATCH_BYTES { + break; + } + batch_bytes += next_bytes; + cursor += 1; + } + let batch = &eligible[start..cursor]; + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + for index in &eligible[start..] { + texts[*index].text = self.replacement.as_str().to_string(); + } + break; + } + match self.sanitize_batch(&texts, batch, remaining) { + Ok(replacements) => { + for (index, replacement) in replacements { + texts[index].text = replacement; + } + } + Err(_) => { + log::warn!( + target: "nemo_relay.plugin", + event = "local_model_provider_failed", + plugin_kind = "pii_redaction", + provider = self.provider_name.as_str(), + batch_size = batch.len(), + reason = "provider_or_response"; + "PII local-model provider failed closed" + ); + for index in batch { + texts[*index].text = self.replacement.as_str().to_string(); + } + } + } + } + texts.into_iter().map(|selected| selected.text).collect() + } + + fn sanitize_batch( + &self, + texts: &[SelectedText], + batch: &[usize], + timeout: Duration, + ) -> PluginResult> { + let request = LocalModelRequest { + version: LOCAL_MODEL_CONTRACT_VERSION, + model_id: self.model_id.as_deref(), + detector_profile: self.detector_profile.as_deref(), + texts: batch + .iter() + .map(|index| LocalModelText { + id: u32::try_from(*index).expect("bounded text index fits u32"), + text: &texts[*index].text, + }) + .collect(), + }; + let request = serde_json::to_value(request)?; + let response = (self.provider)(request, timeout)?; + let response: LocalModelResponse = serde_json::from_value(response).map_err(|error| { + PluginError::RegistrationFailed(format!( + "local-model provider returned an invalid detection response: {error}" + )) + })?; + self.apply_response(texts, batch, response) + } + + fn apply_response( + &self, + texts: &[SelectedText], + batch: &[usize], + response: LocalModelResponse, + ) -> PluginResult> { + if response.version != LOCAL_MODEL_CONTRACT_VERSION { + return Err(PluginError::RegistrationFailed(format!( + "unsupported local-model response version {}", + response.version + ))); + } + if response.detections.len() > batch.len() * MAX_DETECTIONS_PER_TEXT { + return Err(PluginError::RegistrationFailed( + "local-model response exceeded the detection limit".into(), + )); + } + let allowed_ids = batch + .iter() + .map(|index| u32::try_from(*index).expect("bounded text index fits u32")) + .collect::>(); + let mut detections = HashMap::>::new(); + for detection in response.detections { + if !allowed_ids.contains(&detection.text_id) { + return Err(PluginError::RegistrationFailed(format!( + "local-model response referenced unknown text id {}", + detection.text_id + ))); + } + if detection.label.trim().is_empty() || detection.label.len() > 128 { + return Err(PluginError::RegistrationFailed( + "local-model response contained an invalid detection label".into(), + )); + } + if !detection.score.is_finite() || !(0.0..=1.0).contains(&detection.score) { + return Err(PluginError::RegistrationFailed( + "local-model response contained an invalid detection score".into(), + )); + } + let text_detections = detections.entry(detection.text_id).or_default(); + if text_detections.len() >= MAX_DETECTIONS_PER_TEXT { + return Err(PluginError::RegistrationFailed(format!( + "local-model response exceeded the per-text detection limit of {MAX_DETECTIONS_PER_TEXT}" + ))); + } + text_detections.push(detection); + } + let mut replacements = Vec::new(); + for index in batch { + let id = u32::try_from(*index).expect("bounded text index fits u32"); + let Some(mut spans) = detections.remove(&id) else { + continue; + }; + spans.sort_by_key(|span| (span.start_utf8, span.end_utf8)); + let text = &texts[*index].text; + let mut previous_end = 0; + for span in &spans { + if span.start_utf8 >= span.end_utf8 + || span.end_utf8 > text.len() + || !text.is_char_boundary(span.start_utf8) + || !text.is_char_boundary(span.end_utf8) + || span.start_utf8 < previous_end + { + return Err(PluginError::RegistrationFailed( + "local-model response contained invalid or overlapping UTF-8 spans".into(), + )); + } + previous_end = span.end_utf8; + } + spans.retain(|detection| { + detection.score >= self.min_score + && !self.excluded_labels.contains(&detection.label) + }); + if spans.is_empty() { + continue; + } + let mut redacted = text.clone(); + for span in spans.iter().rev() { + redacted.replace_range(span.start_utf8..span.end_utf8, self.replacement.as_str()); + } + replacements.push((*index, redacted)); + } + Ok(replacements) + } + + fn sanitize_request_with_codec(&self, request: &LlmRequest) -> Option { + let codec = self.request_codec.as_ref()?; + let annotated = codec.decode(request).ok()?; + let sanitized = sanitize_serializable(self, annotated).ok()?; + codec.encode(&sanitized, request).ok() + } + + fn sanitize_response_with_codec(&self, payload: Json) -> Option { + let codec = self.response_codec.as_ref()?; + let codec_name = self.codec_name?; + let annotated = codec.decode_response(&payload).ok()?; + let sanitized = sanitize_serializable(self, annotated).ok()?; + Some(codec_name.overlay_response_payload(payload, &sanitized)) + } - let Some(provider) = provider else { + fn codec_failure_payload(&self, direction: &'static str) -> Json { log::warn!( target: "nemo_relay.plugin", - event = "plugin_resource_access_failed", + event = "local_model_codec_failed", plugin_kind = "pii_redaction", - profile = profile_name.unwrap_or("legacy"), - resource_kind = "local_model_backend", - permission = "execute", - reason = "provider_unavailable"; - "Plugin resource access validation failed" + provider = self.provider_name.as_str(), + direction; + "PII local-model codec failed closed" ); - return Err(PluginError::RegistrationFailed( - "PII redaction local-model backend is unavailable in this runtime".to_string(), - )); - }; - log::info!( - target: "nemo_relay.plugin", - event = "plugin_resource_access_pending", - plugin_kind = "pii_redaction", - profile = profile_name.unwrap_or("legacy"), - resource_kind = "local_model_backend", - permission = "execute"; - "Plugin resource access validation started" - ); - let mut scoped_context = profile_name.map(|profile_name| { - PluginRegistrationContext::with_namespace( - ctx.qualify_name(&format!("{}/", profile_registration_prefix(profile_name))), + Json::String(self.replacement.as_str().to_string()) + } +} + +pub(super) fn register_local_backend( + config: PiiRedactionConfig, + ctx: &mut PluginRegistrationContext, + profile_name: Option<&str>, +) -> PluginResult<()> { + let local = config.local.clone().ok_or_else(|| { + PluginError::InvalidConfig( + "local settings are required when mode = 'local_model'".to_string(), ) - }); - let provider_context = scoped_context.as_mut().unwrap_or(ctx); - match provider(config, provider_context) { - Ok(()) => { - if let Some(scoped_context) = scoped_context { - ctx.extend_registrations(scoped_context.into_registrations()); - } - log::info!( - target: "nemo_relay.plugin", - event = "plugin_resource_access_validated", - plugin_kind = "pii_redaction", - profile = profile_name.unwrap_or("legacy"), - resource_kind = "local_model_backend", - permission = "execute"; - "Plugin resource access validated" - ); - Ok(()) + })?; + let backend = CompiledLocalBackend::new(local, config.codec.clone())?; + + if config.mark { + ctx.register_mark_sanitize_guardrail( + ®istration_name(profile_name, "mark"), + config.priority, + event_sanitize_callback(backend.clone(), None), + )?; + } + if config.tool_input { + ctx.register_tool_sanitize_request_guardrail( + ®istration_name(profile_name, "tool_input"), + config.priority, + tool_sanitize_callback(backend.clone()), + )?; + } + if config.tool_output { + ctx.register_tool_sanitize_response_guardrail( + ®istration_name(profile_name, "tool_output"), + config.priority, + tool_sanitize_callback(backend.clone()), + )?; + } + if config.input { + ctx.register_llm_sanitize_request_guardrail( + ®istration_name(profile_name, "input"), + config.priority, + llm_sanitize_request_callback(backend.clone()), + )?; + } + if config.input || config.tool_input { + ctx.register_scope_sanitize_start_guardrail( + ®istration_name( + profile_name, + if profile_name.is_some() { + "scope_start" + } else { + "input" + }, + ), + config.priority, + event_sanitize_callback(backend.clone(), Some((config.input, config.tool_input))), + )?; + } + if config.output { + ctx.register_llm_sanitize_response_guardrail( + ®istration_name(profile_name, "output"), + config.priority, + llm_sanitize_response_callback(backend.clone()), + )?; + } + if config.output || config.tool_output { + ctx.register_scope_sanitize_end_guardrail( + ®istration_name( + profile_name, + if profile_name.is_some() { + "scope_end" + } else { + "output" + }, + ), + config.priority, + event_sanitize_callback(backend, Some((config.output, config.tool_output))), + )?; + } + Ok(()) +} + +fn tool_sanitize_callback(backend: CompiledLocalBackend) -> ToolSanitizeFn { + Arc::new(move |_name, payload| backend.sanitize_json(payload)) +} + +fn event_sanitize_callback( + backend: CompiledLocalBackend, + scope_categories: Option<(bool, bool)>, +) -> EventSanitizeFn { + Arc::new(move |event, mut fields| { + if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { + matches!(event, Event::Scope(_)) + && event + .category() + .is_some_and(|category| match category.as_str() { + "llm" => !sanitize_llm, + "tool" => !sanitize_tool, + _ => false, + }) + }) { + return fields; } - Err(error) => { - if let Some(scoped_context) = scoped_context { - let mut registrations = scoped_context.into_registrations(); - rollback_registrations(&mut registrations); - } - log::warn!( - target: "nemo_relay.plugin", - event = "plugin_resource_access_failed", - plugin_kind = "pii_redaction", - profile = profile_name.unwrap_or("legacy"), - resource_kind = "local_model_backend", - permission = "execute", - reason = "initialization_failed"; - "Plugin resource access validation failed" + let specialized_scope = matches!(event, Event::Scope(_)) + && event + .category() + .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); + if !specialized_scope { + fields.data = fields.data.map(|data| backend.sanitize_json(data)); + fields.category_profile = fields.category_profile.and_then(|profile| { + sanitize_serializable::(&backend, profile).ok() + }); + } + fields.metadata = fields + .metadata + .map(|metadata| backend.sanitize_json(metadata)); + fields + }) +} + +fn llm_sanitize_request_callback(backend: CompiledLocalBackend) -> LlmSanitizeRequestFn { + Arc::new(move |mut request| { + if backend.request_codec.is_some() { + request.content = match backend.sanitize_request_with_codec(&request) { + Some(encoded) => return encoded, + None => backend.codec_failure_payload("request"), + }; + return request; + } + request.content = backend.sanitize_json(request.content); + request + }) +} + +fn llm_sanitize_response_callback(backend: CompiledLocalBackend) -> LlmSanitizeResponseFn { + Arc::new(move |payload| { + if backend.response_codec.is_some() { + return backend + .sanitize_response_with_codec(payload) + .unwrap_or_else(|| backend.codec_failure_payload("response")); + } + backend.sanitize_json(payload) + }) +} + +fn sanitize_serializable(backend: &CompiledLocalBackend, value: T) -> PluginResult +where + T: Serialize + DeserializeOwned, +{ + let value = serde_json::to_value(value)?; + serde_json::from_value(backend.sanitize_json(value)).map_err(PluginError::from) +} + +fn registration_name(profile_name: Option<&str>, callback_name: &str) -> String { + profile_name.map_or_else( + || callback_name.to_string(), + |profile_name| { + format!( + "{}/{callback_name}", + profile_registration_prefix(profile_name) + ) + }, + ) +} + +pub(super) struct LocalConfigViolation { + pub(super) field: &'static str, + pub(super) message: String, +} + +pub(super) fn validate_local_backend_config( + config: &LocalBackendConfig, +) -> Vec { + let mut violations = Vec::new(); + let mut push = |field, message| violations.push(LocalConfigViolation { field, message }); + + match config.backend.as_deref().map(str::trim) { + None | Some("") => push( + "local.backend", + "local.backend is required when mode = 'local_model'".into(), + ), + Some(backend) if backend.len() > MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES => push( + "local.backend", + format!( + "local.backend must not exceed {MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES} UTF-8 bytes" + ), + ), + Some(_) => {} + } + if config.allow_network == Some(true) { + push( + "local.allow_network", + "worker-backed local-model providers must not use network inference".into(), + ); + } + match config.max_latency_ms { + Some(0) => push( + "local.max_latency_ms", + "local.max_latency_ms must be greater than zero".into(), + ), + Some(latency) if latency > MAX_LOCAL_MODEL_LATENCY_MS => push( + "local.max_latency_ms", + format!("local.max_latency_ms must not exceed {MAX_LOCAL_MODEL_LATENCY_MS}"), + ), + _ => {} + } + for (field, value) in [ + ("local.model_id", config.model_id.as_deref()), + ("local.detector_profile", config.detector_profile.as_deref()), + ] { + if value.is_some_and(|value| value.trim().is_empty()) { + push(field, format!("{field} must be a non-empty string")); + } else if value.is_some_and(|value| value.len() > MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES) { + push( + field, + format!( + "{field} must not exceed {MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES} UTF-8 bytes" + ), ); - Err(error) } } + if config.target_paths.len() + config.target_path_patterns.len() > MAX_LOCAL_MODEL_TARGET_PATHS + { + push( + "local.target_paths", + format!( + "local.target_paths and local.target_path_patterns must contain at most {MAX_LOCAL_MODEL_TARGET_PATHS} entries in total" + ), + ); + } + if config + .target_paths + .iter() + .any(|path| path.len() > MAX_LOCAL_MODEL_TARGET_PATH_BYTES) + { + push( + "local.target_paths", + format!( + "local.target_paths entries must not exceed {MAX_LOCAL_MODEL_TARGET_PATH_BYTES} UTF-8 bytes" + ), + ); + } + if config + .target_paths + .iter() + .any(|path| !is_valid_json_pointer(path)) + { + push( + "local.target_paths", + "local.target_paths entries must be valid JSON pointers".into(), + ); + } + if config + .target_path_patterns + .iter() + .any(|path| path.len() > MAX_LOCAL_MODEL_TARGET_PATH_BYTES) + { + push( + "local.target_path_patterns", + format!( + "local.target_path_patterns entries must not exceed {MAX_LOCAL_MODEL_TARGET_PATH_BYTES} UTF-8 bytes" + ), + ); + } + if config + .target_path_patterns + .iter() + .any(|path| !is_valid_json_pointer_pattern(path)) + { + push( + "local.target_path_patterns", + "local.target_path_patterns entries must be valid JSON-pointer patterns".into(), + ); + } + if config + .min_score + .is_some_and(|score| !score.is_finite() || !(0.0..=1.0).contains(&score)) + { + push( + "local.min_score", + "local.min_score must be a finite number between 0 and 1".into(), + ); + } + if config.excluded_labels.len() > MAX_LOCAL_MODEL_EXCLUDED_LABELS { + push( + "local.excluded_labels", + format!( + "local.excluded_labels must contain at most {MAX_LOCAL_MODEL_EXCLUDED_LABELS} entries" + ), + ); + } + if config + .excluded_labels + .iter() + .any(|label| label.trim().is_empty() || label.len() > MAX_LOCAL_MODEL_LABEL_BYTES) + { + push( + "local.excluded_labels", + format!( + "local.excluded_labels entries must be non-empty and at most {MAX_LOCAL_MODEL_LABEL_BYTES} UTF-8 bytes" + ), + ); + } + if config + .excluded_labels + .iter() + .map(|label| label.trim()) + .collect::>() + .len() + != config.excluded_labels.len() + { + push( + "local.excluded_labels", + "local.excluded_labels must not contain duplicates".into(), + ); + } + if config + .replacement + .as_ref() + .is_some_and(|replacement| replacement.len() > MAX_LOCAL_MODEL_REPLACEMENT_BYTES) + { + push( + "local.replacement", + format!( + "local.replacement must not exceed {MAX_LOCAL_MODEL_REPLACEMENT_BYTES} UTF-8 bytes" + ), + ); + } + + violations } + +#[cfg(test)] +#[path = "../tests/unit/local_tests.rs"] +mod tests; diff --git a/crates/pii-redaction/tests/unit/component_tests.rs b/crates/pii-redaction/tests/unit/component_tests.rs index cd7ad4026..6f25a970c 100644 --- a/crates/pii-redaction/tests/unit/component_tests.rs +++ b/crates/pii-redaction/tests/unit/component_tests.rs @@ -30,8 +30,10 @@ use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::plugin::{ ConfigPolicy, DiagnosticLevel, PluginComponentSpec, PluginConfig, PluginError, PluginRegistrationContext, UnsupportedBehavior, clear_plugin_configuration, + deregister_local_model_provider, ensure_builtin_plugins_registered, initialize_plugins_exact as initialize_plugins, - list_plugin_kinds, rollback_registrations, validate_plugin_config, + list_plugin_kinds, register_local_model_provider_tracked, rollback_registrations, + validate_plugin_config, }; use futures::StreamExt; use nemo_relay::observability::atif::{AtifAgentInfo, AtifExporter}; @@ -140,13 +142,34 @@ fn top_level_policy_controls_component_diagnostics() { fn reset_runtime() { enable_operational_logs(); let _ = clear_plugin_configuration(); - crate::plugins::pii_redaction::component::clear_local_backend_provider().unwrap(); crate::shared_runtime::reset_runtime_owner_for_tests(); let context = global_context(); *context.write().unwrap() = NemoRelayContextState::new(); register_pii_redaction_component().unwrap(); } +struct LocalModelProviderGuard { + name: String, + registration_id: u64, +} + +impl Drop for LocalModelProviderGuard { + fn drop(&mut self) { + let _ = deregister_local_model_provider(&self.name, self.registration_id); + } +} + +fn register_test_local_model_provider( + name: &str, + callback: impl Fn(Json, std::time::Duration) -> Result + Send + Sync + 'static, +) -> LocalModelProviderGuard { + let registration_id = register_local_model_provider_tracked(name, Arc::new(callback)).unwrap(); + LocalModelProviderGuard { + name: name.to_string(), + registration_id, + } +} + fn setup_isolated_thread() { let stack = create_scope_stack(); set_thread_scope_stack(stack); @@ -1257,6 +1280,76 @@ fn profile_array_executes_every_profile_in_stable_array_order() { clear_plugin_configuration().unwrap(); } +#[test] +fn deterministic_and_local_model_profiles_compose_in_priority_order() { + let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); + reset_runtime(); + setup_isolated_thread(); + + let _provider = register_test_local_model_provider("contextual", |request, _| { + assert_eq!( + request["texts"][0]["text"], "Alice emailed [REDACTED]", + "the local provider must receive the deterministic profile's output" + ); + Ok(json!({ + "version": 1, + "detections": [{ + "text_id": 0, + "start_utf8": 0, + "end_utf8": 5, + "label": "GIVEN_NAME", + "score": 0.99 + }] + })) + }); + + futures::executor::block_on(initialize_plugins(plugin_config(json!({ + "codec": "openai_chat", + "profiles": [ + { + "mode": "builtin", + "priority": 80, + "builtin": { + "action": "redact", + "detector": "email" + } + }, + { + "mode": "local_model", + "priority": 90, + "local": { + "backend": "contextual", + "target_paths": ["/message"] + } + } + ] + })))) + .unwrap(); + + let events = capture_events("pii-profile-composition"); + event( + EmitMarkEventParams::builder() + .name("composed-profile-mark") + .data(json!({ + "message": "Alice emailed alice@example.com", + "region": "us-west-2" + })) + .build(), + ) + .unwrap(); + let captured = captured_events_snapshot(&events); + assert_eq!( + captured[0].data().unwrap(), + &json!({ + "message": "[REDACTED] emailed [REDACTED]", + "region": "us-west-2" + }) + ); + + deregister_subscriber("pii-profile-composition").unwrap(); + clear_plugin_configuration().unwrap(); +} + #[test] fn profile_array_rejects_legacy_fields_and_reports_profile_paths() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); @@ -1364,10 +1457,12 @@ fn local_profile_registrations_receive_generated_namespaces() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); - register_local_backend_provider(Arc::new(|_, ctx| { - ctx.register_mark_sanitize_guardrail("shared", 100, Arc::new(|_, fields| fields)) - })) - .unwrap(); + let _one = register_test_local_model_provider("one", |_, _| { + Ok(json!({"version": 1, "detections": []})) + }); + let _two = register_test_local_model_provider("two", |_, _| { + Ok(json!({"version": 1, "detections": []})) + }); let plugin = PiiRedactionPlugin; let config = json!({ @@ -1384,8 +1479,8 @@ fn local_profile_registrations_receive_generated_namespaces() { futures::executor::block_on(plugin.register(&config, &mut ctx)).unwrap(); let mut registrations = ctx.into_registrations(); let registrations_debug = format!("{registrations:?}"); - assert!(registrations_debug.contains("profiles::profile_00000000000000000000/shared")); - assert!(registrations_debug.contains("profiles::profile_00000000000000000001/shared")); + assert!(registrations_debug.contains("profiles::profile_00000000000000000000/mark")); + assert!(registrations_debug.contains("profiles::profile_00000000000000000001/mark")); rollback_registrations(&mut registrations); assert!(registrations.is_empty()); } @@ -1396,12 +1491,6 @@ fn failed_later_profile_rolls_back_earlier_profile_registrations() { reset_runtime(); setup_isolated_thread(); - register_local_backend_provider(Arc::new(|_, _| { - Err(PluginError::RegistrationFailed( - "intentional profile failure".into(), - )) - })) - .unwrap(); let activation = futures::executor::block_on(initialize_plugins(plugin_config(json!({ "codec": "openai_chat", "profiles": [ @@ -1696,6 +1785,112 @@ fn validate_rejects_local_section_outside_local_mode() { })); } +#[test] +fn validate_rejects_invalid_local_model_provider_settings() { + let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); + reset_runtime(); + + let cases = [ + ( + json!({"mode": "local_model"}), + "local", + "required when mode = 'local_model'", + ), + ( + json!({"mode": "local_model", "local": {"backend": " "}}), + "local.backend", + "local.backend is required", + ), + ( + json!({ + "mode": "local_model", + "local": { + "backend": "x".repeat(MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES + 1) + } + }), + "local.backend", + "must not exceed", + ), + ( + json!({ + "mode": "local_model", + "local": {"backend": "worker", "allow_network": true} + }), + "local.allow_network", + "must not use network inference", + ), + ( + json!({ + "mode": "local_model", + "local": {"backend": "worker", "max_latency_ms": 0} + }), + "local.max_latency_ms", + "must be greater than zero", + ), + ( + json!({ + "mode": "local_model", + "local": {"backend": "worker", "max_latency_ms": 60001} + }), + "local.max_latency_ms", + "must not exceed 60000", + ), + ( + json!({ + "mode": "local_model", + "local": {"backend": "worker", "model_id": " "} + }), + "local.model_id", + "must be a non-empty string", + ), + ( + json!({ + "mode": "local_model", + "local": {"backend": "worker", "target_paths": ["message"]} + }), + "local.target_paths", + "valid JSON pointers", + ), + ( + json!({ + "mode": "local_model", + "local": { + "backend": "worker", + "replacement": "x".repeat(MAX_LOCAL_MODEL_REPLACEMENT_BYTES + 1) + } + }), + "local.replacement", + "must not exceed", + ), + ]; + + for (config, field, message) in cases { + let report = validate_plugin_config(&plugin_config(config)); + assert!(report.diagnostics.iter().any(|diagnostic| { + diagnostic.field.as_deref() == Some(field) && diagnostic.message.contains(message) + })); + } +} + +#[test] +fn validate_warns_when_local_model_inspects_every_string_leaf() { + let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); + reset_runtime(); + + let report = validate_plugin_config(&plugin_config(json!({ + "mode": "local_model", + "codec": "openai_chat", + "local": {"backend": "worker"} + }))); + + assert!(!report.has_errors(), "{:?}", report.diagnostics); + assert!(report.diagnostics.iter().any(|diagnostic| { + diagnostic.level == DiagnosticLevel::Warning + && diagnostic.code == "pii_redaction.local_model_all_paths" + && diagnostic.field.as_deref() == Some("local.target_paths") + })); +} + #[test] fn validate_rejects_builtin_mode_without_builtin_section() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); @@ -1832,28 +2027,31 @@ fn local_backend_provider_is_invoked_for_local_model_mode() { let called = Arc::new(AtomicBool::new(false)); let called_inner = Arc::clone(&called); - register_local_backend_provider(Arc::new( - move |config, _ctx: &mut PluginRegistrationContext| { - called_inner.store(true, Ordering::SeqCst); - assert_eq!(config.mode, "local_model"); - Ok(()) - }, - )) - .unwrap(); - - let plugin = PiiRedactionPlugin; - let mut ctx = PluginRegistrationContext::with_namespace("test::"); - let config = json!({ + let _provider = register_test_local_model_provider("test-provider", move |request, _| { + called_inner.store(true, Ordering::SeqCst); + assert_eq!(request["version"], 1); + Ok(json!({"version": 1, "detections": []})) + }); + setup_isolated_thread(); + futures::executor::block_on(initialize_plugins(plugin_config(json!({ "mode": "local_model", + "input": false, + "output": false, + "mark": false, "tool_input": true, - }); - let Json::Object(config) = config else { - panic!("component config must be object"); - }; - - futures::executor::block_on(plugin.register(&config, &mut ctx)).unwrap(); - + "tool_output": false, + "local": {"backend": "test-provider"} + })))) + .unwrap(); + tool_call( + ToolCallParams::builder() + .name("test") + .args(json!({"text": "hello"})) + .build(), + ) + .unwrap(); assert!(called.load(Ordering::SeqCst)); + clear_plugin_configuration().unwrap(); } #[test] @@ -1862,7 +2060,15 @@ fn local_backend_reports_missing_and_failed_provider_initialization() { reset_runtime(); let plugin = PiiRedactionPlugin; - let config = json!({"mode": "local_model"}); + let config = json!({ + "mode": "local_model", + "input": false, + "output": false, + "mark": false, + "tool_input": true, + "tool_output": false, + "local": {"backend": "missing"} + }); let Json::Object(config) = config else { panic!("component config must be object"); }; @@ -1871,20 +2077,26 @@ fn local_backend_reports_missing_and_failed_provider_initialization() { .expect_err("missing local provider should fail registration"); assert!(missing.to_string().contains("unavailable")); - register_local_backend_provider(Arc::new(|_, _| { - Err(PluginError::RegistrationFailed( - "provider initialization failed".into(), - )) - })) - .unwrap(); + let _failed = register_test_local_model_provider("failed", |_, _| { + Err(PluginError::RegistrationFailed("provider failed".into())) + }); + let config = json!({ + "mode": "local_model", + "input": false, + "output": false, + "mark": false, + "tool_input": true, + "tool_output": false, + "local": {"backend": "failed"} + }); + let Json::Object(config) = config else { + panic!("component config must be object"); + }; let mut ctx = PluginRegistrationContext::with_namespace("failed::"); - let failed = futures::executor::block_on(plugin.register(&config, &mut ctx)) - .expect_err("failed local provider should fail registration"); - assert!( - failed - .to_string() - .contains("provider initialization failed") - ); + futures::executor::block_on(plugin.register(&config, &mut ctx)) + .expect("provider availability should be checked at registration"); + let mut registrations = ctx.into_registrations(); + rollback_registrations(&mut registrations); } #[test] diff --git a/crates/pii-redaction/tests/unit/local_tests.rs b/crates/pii-redaction/tests/unit/local_tests.rs new file mode 100644 index 000000000..6fd2db179 --- /dev/null +++ b/crates/pii-redaction/tests/unit/local_tests.rs @@ -0,0 +1,798 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::atomic::{AtomicUsize, Ordering}; + +use nemo_relay::plugin::{deregister_local_model_provider, register_local_model_provider_tracked}; +use serde_json::json; + +use super::*; + +struct ProviderGuard { + name: &'static str, + registration_id: u64, +} + +impl Drop for ProviderGuard { + fn drop(&mut self) { + let _ = deregister_local_model_provider(self.name, self.registration_id); + } +} + +fn backend( + name: &'static str, + callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, +) -> (ProviderGuard, CompiledLocalBackend) { + let registration_id = register_local_model_provider_tracked(name, Arc::new(callback)).unwrap(); + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some(name.into()), + ..LocalBackendConfig::default() + }, + None, + ) + .unwrap(); + ( + ProviderGuard { + name, + registration_id, + }, + backend, + ) +} + +fn alice_detector(request: Json, _timeout: Duration) -> PluginResult { + let mut detections = Vec::new(); + for item in request["texts"] + .as_array() + .expect("provider request should contain texts") + { + let text_id = item["id"].as_u64().expect("text id should be an integer"); + let text = item["text"].as_str().expect("text should be a string"); + for (start, value) in text.match_indices("Alice") { + detections.push(json!({ + "text_id": text_id, + "start_utf8": start, + "end_utf8": start + value.len(), + "label": "given_name", + "score": 0.99 + })); + } + } + Ok(json!({"version": 1, "detections": detections})) +} + +#[test] +fn applies_non_overlapping_utf8_byte_spans() { + let (_provider, backend) = backend("local-test-utf8", |_, _| { + Ok(json!({ + "version": 1, + "detections": [ + { + "text_id": 0, + "start_utf8": 0, + "end_utf8": 5, + "label": "given_name", + "score": 0.99 + }, + { + "text_id": 0, + "start_utf8": 6, + "end_utf8": 12, + "label": "surname", + "score": 0.98 + } + ] + })) + }); + + assert_eq!( + backend.sanitize_json(json!({"text": "José Rivera"})), + json!({"text": "[REDACTED] [REDACTED]"}) + ); +} + +#[test] +fn malformed_or_overlapping_spans_fail_closed_for_the_batch() { + let (_provider, backend) = backend("local-test-overlap", |_, _| { + Ok(json!({ + "version": 1, + "detections": [ + { + "text_id": 0, + "start_utf8": 0, + "end_utf8": 4, + "label": "name", + "score": 0.9 + }, + { + "text_id": 0, + "start_utf8": 3, + "end_utf8": 6, + "label": "name", + "score": 0.9 + } + ] + })) + }); + + assert_eq!( + backend.sanitize_json(json!({"first": "secret", "second": "safe"})), + json!({"first": "[REDACTED]", "second": "[REDACTED]"}) + ); +} + +#[test] +fn provider_errors_fail_closed_without_changing_unselected_paths() { + let registration_id = register_local_model_provider_tracked( + "local-test-failure", + Arc::new(|_, _| Err(PluginError::RegistrationFailed("boom".into()))), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-failure", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-failure".into()), + target_paths: vec!["/selected".into()], + replacement: Some("[PRIVATE]".into()), + ..LocalBackendConfig::default() + }, + None, + ) + .unwrap(); + + assert_eq!( + backend.sanitize_json(json!({ + "selected": "secret", + "unselected": "preserve" + })), + json!({ + "selected": "[PRIVATE]", + "unselected": "preserve" + }) + ); +} + +#[test] +fn batches_provider_requests_and_preserves_no_detection_values() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let (_provider, backend) = backend("local-test-batching", move |request, _| { + observed.fetch_add(1, Ordering::SeqCst); + assert!(request["texts"].as_array().unwrap().len() <= MAX_BATCH_ITEMS); + Ok(json!({"version": 1, "detections": []})) + }); + let values = (0..(MAX_BATCH_ITEMS + 1)) + .map(|index| Json::String(format!("value-{index}"))) + .collect(); + + let sanitized = backend.sanitize_json(Json::Array(values)); + + assert_eq!(sanitized[0], "value-0"); + assert_eq!( + sanitized[MAX_BATCH_ITEMS], + format!("value-{MAX_BATCH_ITEMS}") + ); + assert_eq!(calls.load(Ordering::SeqCst), 2); +} + +#[test] +fn latency_budget_applies_to_the_entire_payload() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let registration_id = register_local_model_provider_tracked( + "local-test-total-deadline", + Arc::new(move |_, timeout| { + observed.fetch_add(1, Ordering::SeqCst); + std::thread::sleep(timeout + Duration::from_millis(5)); + Err(PluginError::RegistrationFailed("timed out".into())) + }), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-total-deadline", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-total-deadline".into()), + max_latency_ms: Some(10), + ..LocalBackendConfig::default() + }, + None, + ) + .unwrap(); + let values = (0..(MAX_BATCH_ITEMS + 1)) + .map(|index| Json::String(format!("value-{index}"))) + .collect(); + + let sanitized = backend.sanitize_json(Json::Array(values)); + + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert!( + sanitized + .as_array() + .unwrap() + .iter() + .all(|value| value == "[REDACTED]") + ); +} + +#[test] +fn oversized_text_is_redacted_without_calling_the_provider() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let (_provider, backend) = backend("local-test-oversized", move |_, _| { + observed.fetch_add(1, Ordering::SeqCst); + Ok(json!({"version": 1, "detections": []})) + }); + + assert_eq!( + backend.sanitize_json(Json::String("x".repeat(MAX_TEXT_BYTES + 1))), + Json::String("[REDACTED]".into()) + ); + assert_eq!(calls.load(Ordering::SeqCst), 0); +} + +#[test] +fn oversized_text_does_not_shift_later_provider_results() { + let (_provider, backend) = backend("local-test-oversized-middle", |_, _| { + Ok(json!({"version": 1, "detections": []})) + }); + + assert_eq!( + backend.sanitize_json(json!(["first", "x".repeat(MAX_TEXT_BYTES + 1), "third"])), + json!(["first", "[REDACTED]", "third"]) + ); +} + +#[test] +fn payload_count_limit_fails_closed_without_unbounded_provider_calls() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let (_provider, backend) = backend("local-test-count-limit", move |_, _| { + observed.fetch_add(1, Ordering::SeqCst); + Ok(json!({"version": 1, "detections": []})) + }); + let values = (0..(MAX_TEXTS_PER_PAYLOAD + 1)) + .map(|index| Json::String(format!("value-{index}"))) + .collect(); + + let sanitized = backend.sanitize_json(Json::Array(values)); + + assert_eq!(sanitized[0], "value-0"); + assert_eq!(sanitized[MAX_TEXTS_PER_PAYLOAD], "[REDACTED]"); + assert_eq!( + calls.load(Ordering::SeqCst), + MAX_TEXTS_PER_PAYLOAD.div_ceil(MAX_BATCH_ITEMS) + ); +} + +#[test] +fn payload_byte_limit_fails_closed_after_the_bounded_prefix() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let (_provider, backend) = backend("local-test-byte-limit", move |_, _| { + observed.fetch_add(1, Ordering::SeqCst); + Ok(json!({"version": 1, "detections": []})) + }); + let accepted = MAX_PAYLOAD_TEXT_BYTES / MAX_TEXT_BYTES; + let values = (0..=accepted) + .map(|_| Json::String("x".repeat(MAX_TEXT_BYTES))) + .collect(); + + let sanitized = backend.sanitize_json(Json::Array(values)); + + assert_eq!(sanitized[accepted - 1], "x".repeat(MAX_TEXT_BYTES)); + assert_eq!(sanitized[accepted], "[REDACTED]"); + assert_eq!( + calls.load(Ordering::SeqCst), + MAX_PAYLOAD_TEXT_BYTES.div_ceil(MAX_BATCH_BYTES) + ); +} + +#[test] +fn non_utf8_boundary_detection_fails_closed() { + let (_provider, backend) = backend("local-test-utf8-boundary", |_, _| { + Ok(json!({ + "version": 1, + "detections": [{ + "text_id": 0, + "start_utf8": 1, + "end_utf8": 2, + "label": "invalid", + "score": 1.0 + }] + })) + }); + + assert_eq!( + backend.sanitize_json(json!("é")), + Json::String("[REDACTED]".into()) + ); +} + +#[test] +fn local_policy_rejects_malformed_or_unbounded_values() { + let registration_id = register_local_model_provider_tracked( + "local-test-policy-bounds", + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-policy-bounds", + registration_id, + }; + + for (config, expected) in [ + ( + LocalBackendConfig { + backend: Some("x".repeat(MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES + 1)), + ..LocalBackendConfig::default() + }, + "local.backend", + ), + ( + LocalBackendConfig { + backend: Some("local-test-policy-bounds".into()), + target_paths: vec!["message".into()], + ..LocalBackendConfig::default() + }, + "valid JSON pointer", + ), + ( + LocalBackendConfig { + backend: Some("local-test-policy-bounds".into()), + target_paths: vec!["/bad~escape".into()], + ..LocalBackendConfig::default() + }, + "valid JSON pointer", + ), + ( + LocalBackendConfig { + backend: Some("local-test-policy-bounds".into()), + target_path_patterns: vec!["messages/*/content".into()], + ..LocalBackendConfig::default() + }, + "valid JSON-pointer pattern", + ), + ( + LocalBackendConfig { + backend: Some("local-test-policy-bounds".into()), + replacement: Some("x".repeat(MAX_LOCAL_MODEL_REPLACEMENT_BYTES + 1)), + ..LocalBackendConfig::default() + }, + "local.replacement", + ), + ( + LocalBackendConfig { + backend: Some("local-test-policy-bounds".into()), + model_id: Some("x".repeat(MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES + 1)), + ..LocalBackendConfig::default() + }, + "local.model_id", + ), + ( + LocalBackendConfig { + backend: Some("local-test-policy-bounds".into()), + min_score: Some(f64::NAN), + ..LocalBackendConfig::default() + }, + "local.min_score", + ), + ( + LocalBackendConfig { + backend: Some("local-test-policy-bounds".into()), + excluded_labels: vec!["NAME".into(), "NAME".into()], + ..LocalBackendConfig::default() + }, + "local.excluded_labels", + ), + ] { + let error = CompiledLocalBackend::new(config, None) + .err() + .expect("invalid local policy should fail"); + assert!(error.to_string().contains(expected), "{error}"); + } +} + +#[test] +fn local_policy_accepts_root_and_escaped_json_pointers() { + assert!(is_valid_json_pointer("")); + assert!(is_valid_json_pointer("/nested/a~1b/~0value")); +} + +#[test] +fn target_path_patterns_match_one_segment_without_widening_exact_paths() { + let registration_id = + register_local_model_provider_tracked("local-test-path-patterns", Arc::new(alice_detector)) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-path-patterns", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-path-patterns".into()), + target_paths: vec!["/exact".into()], + target_path_patterns: vec!["/messages/*/content".into()], + ..LocalBackendConfig::default() + }, + None, + ) + .unwrap(); + + assert_eq!( + backend.sanitize_json(json!({ + "exact": "Alice", + "messages": [ + {"content": "Alice", "name": "Alice"}, + {"content": "Alice"} + ], + "nested": {"messages": [{"content": "Alice"}]} + })), + json!({ + "exact": "[REDACTED]", + "messages": [ + {"content": "[REDACTED]", "name": "Alice"}, + {"content": "[REDACTED]"} + ], + "nested": {"messages": [{"content": "Alice"}]} + }) + ); +} + +#[test] +fn request_codec_classifies_only_normalized_content_patterns() { + let registration_id = register_local_model_provider_tracked( + "local-test-openai-request", + Arc::new(alice_detector), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-openai-request", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-openai-request".into()), + target_path_patterns: vec![ + "/messages/*/content".into(), + "/messages/*/content/*/text".into(), + ], + ..LocalBackendConfig::default() + }, + Some("openai_chat".into()), + ) + .unwrap(); + let request = LlmRequest { + headers: serde_json::Map::new(), + content: json!({ + "model": "Alice-model", + "trace_id": "Alice-trace", + "messages": [ + {"role": "system", "content": "Keep this policy"}, + { + "role": "user", + "content": [ + {"type": "text", "text": "Email Alice"}, + {"type": "image_url", "image_url": {"url": "https://Alice.invalid"}} + ] + } + ] + }), + }; + let codec = backend + .request_codec + .as_ref() + .expect("configured request codec should exist"); + let annotated = codec + .decode(&request) + .expect("OpenAI request should decode"); + let sanitized_annotated = + sanitize_serializable(&backend, annotated).expect("annotated request should sanitize"); + codec + .encode(&sanitized_annotated, &request) + .expect("sanitized OpenAI request should encode"); + + let sanitized = llm_sanitize_request_callback(backend)(request); + + assert_eq!(sanitized.content["model"], "Alice-model"); + assert_eq!(sanitized.content["trace_id"], "Alice-trace"); + assert_eq!( + sanitized.content["messages"][0]["content"], + "Keep this policy" + ); + assert_eq!( + sanitized.content["messages"][1]["content"][0]["text"], + "Email [REDACTED]" + ); + assert_eq!( + sanitized.content["messages"][1]["content"][1]["image_url"]["url"], + "https://Alice.invalid" + ); +} + +#[test] +fn response_codec_classifies_message_content_without_touching_identity_fields() { + let registration_id = register_local_model_provider_tracked( + "local-test-openai-response", + Arc::new(alice_detector), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-openai-response", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-openai-response".into()), + target_path_patterns: vec!["/message".into(), "/message/*/text".into()], + ..LocalBackendConfig::default() + }, + Some("openai_chat".into()), + ) + .unwrap(); + let response = json!({ + "id": "Alice-response", + "model": "Alice-model", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": "Hello Alice"}, + "finish_reason": "stop" + }], + "vendor_trace": "Alice-trace" + }); + + let sanitized = backend + .sanitize_response_with_codec(response) + .expect("configured codec should sanitize the response"); + + assert_eq!(sanitized["id"], "Alice-response"); + assert_eq!(sanitized["model"], "Alice-model"); + assert_eq!(sanitized["vendor_trace"], "Alice-trace"); + assert_eq!( + sanitized["choices"][0]["message"]["content"], + "Hello [REDACTED]" + ); +} + +#[test] +fn request_codec_failure_replaces_the_observable_body() { + let registration_id = register_local_model_provider_tracked( + "local-test-invalid-openai-request", + Arc::new(alice_detector), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-invalid-openai-request", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-invalid-openai-request".into()), + target_path_patterns: vec!["/messages/*/content".into()], + replacement: Some("[PRIVATE]".into()), + ..LocalBackendConfig::default() + }, + Some("openai_chat".into()), + ) + .unwrap(); + let request = LlmRequest { + headers: serde_json::Map::from_iter([( + "x-provider-id".into(), + Json::String("preserve-header".into()), + )]), + content: json!({ + "messages": "Alice cannot be decoded as an OpenAI message list", + "vendor_trace": "Alice-trace" + }), + }; + + let sanitized = llm_sanitize_request_callback(backend)(request); + + assert_eq!(sanitized.content, json!("[PRIVATE]")); + assert_eq!(sanitized.headers["x-provider-id"], "preserve-header"); +} + +#[test] +fn request_codec_ambiguous_multi_message_edit_fails_closed() { + let registration_id = register_local_model_provider_tracked( + "local-test-ambiguous-openai-request", + Arc::new(alice_detector), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-ambiguous-openai-request", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-ambiguous-openai-request".into()), + target_path_patterns: vec!["/messages/*/content".into()], + replacement: Some("[PRIVATE]".into()), + ..LocalBackendConfig::default() + }, + Some("openai_chat".into()), + ) + .unwrap(); + let request = LlmRequest { + headers: serde_json::Map::new(), + content: json!({ + "messages": [ + {"role": "system", "content": "Alice owns this policy"}, + {"role": "user", "content": "Email Alice"} + ] + }), + }; + + let sanitized = llm_sanitize_request_callback(backend)(request); + + assert_eq!(sanitized.content, json!("[PRIVATE]")); +} + +#[test] +fn response_codec_failure_replaces_the_observable_payload() { + let registration_id = register_local_model_provider_tracked( + "local-test-invalid-openai-response", + Arc::new(alice_detector), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-invalid-openai-response", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-invalid-openai-response".into()), + target_path_patterns: vec!["/message".into()], + replacement: Some("[PRIVATE]".into()), + ..LocalBackendConfig::default() + }, + Some("openai_chat".into()), + ) + .unwrap(); + let response = json!({ + "choices": "Alice cannot be decoded as an OpenAI response list", + "vendor_trace": "Alice-trace" + }); + + let sanitized = llm_sanitize_response_callback(backend)(response); + + assert_eq!(sanitized, json!("[PRIVATE]")); +} + +#[test] +fn host_policy_applies_score_threshold_and_label_exclusions() { + let registration_id = register_local_model_provider_tracked( + "local-test-detection-policy", + Arc::new(|_, _| { + Ok(json!({ + "version": 1, + "detections": [ + { + "text_id": 0, + "start_utf8": 0, + "end_utf8": 5, + "label": "LOW_SCORE", + "score": 0.49 + }, + { + "text_id": 0, + "start_utf8": 6, + "end_utf8": 11, + "label": "PRESERVE", + "score": 0.99 + }, + { + "text_id": 0, + "start_utf8": 12, + "end_utf8": 17, + "label": "REDACT", + "score": 0.99 + } + ] + })) + }), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-detection-policy", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-detection-policy".into()), + min_score: Some(0.5), + excluded_labels: vec!["PRESERVE".into()], + ..LocalBackendConfig::default() + }, + None, + ) + .unwrap(); + + assert_eq!( + backend.sanitize_json(json!("first next1 final")), + json!("first next1 [REDACTED]") + ); +} + +#[test] +fn validates_filtered_detections_before_applying_host_policy() { + let registration_id = register_local_model_provider_tracked( + "local-test-filtered-invalid-span", + Arc::new(|_, _| { + Ok(json!({ + "version": 1, + "detections": [{ + "text_id": 0, + "start_utf8": 0, + "end_utf8": 999, + "label": "LOW_SCORE", + "score": 0.1 + }] + })) + }), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-filtered-invalid-span", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-filtered-invalid-span".into()), + min_score: Some(0.5), + ..LocalBackendConfig::default() + }, + None, + ) + .unwrap(); + + assert_eq!( + backend.sanitize_json(json!("preserve without detections")), + json!("[REDACTED]") + ); +} + +#[test] +fn enforces_detection_limit_for_each_text() { + let registration_id = register_local_model_provider_tracked( + "local-test-per-text-detection-limit", + Arc::new(|_, _| { + let detections = (0..=MAX_DETECTIONS_PER_TEXT) + .map(|index| { + json!({ + "text_id": 0, + "start_utf8": index, + "end_utf8": index + 1, + "label": "NAME", + "score": 0.9 + }) + }) + .collect::>(); + Ok(json!({"version": 1, "detections": detections})) + }), + ) + .unwrap(); + let _provider = ProviderGuard { + name: "local-test-per-text-detection-limit", + registration_id, + }; + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-per-text-detection-limit".into()), + ..LocalBackendConfig::default() + }, + None, + ) + .unwrap(); + + assert_eq!( + backend.sanitize_json(json!(["x".repeat(MAX_DETECTIONS_PER_TEXT + 1), "second"])), + json!(["[REDACTED]", "[REDACTED]"]) + ); +} diff --git a/docs/about-nemo-relay/release-notes/index.mdx b/docs/about-nemo-relay/release-notes/index.mdx index a87eb2176..55663a6a1 100644 --- a/docs/about-nemo-relay/release-notes/index.mdx +++ b/docs/about-nemo-relay/release-notes/index.mdx @@ -96,8 +96,10 @@ For the new callback contract and codec operations, refer to - The NeMo Guardrails remote backend inherits its configured service's availability, latency, and policy behavior. The local backend requires Python 3.11 or later and `nemoguardrails==0.22.0`. -- The PII redaction plugin currently supports its deterministic local backend; - local-model backend configuration is reserved for future work. +- The PII redaction plugin supports deterministic built-in policies and + worker-backed local-model providers. Local-model mode requires a separately + installed compatible `grpc-v1` provider; the optional Rampart provider + supports Latin-script text and does not provide complete PII coverage. - Pricing and optimization estimates depend on model names, token data, pricing sources, and freshness evidence. Missing or inconsistent evidence produces partial or absent cost fields rather than zero values. diff --git a/docs/configure-plugins/pii-redaction/about.mdx b/docs/configure-plugins/pii-redaction/about.mdx index a37cd4812..cd2ecca78 100644 --- a/docs/configure-plugins/pii-redaction/about.mdx +++ b/docs/configure-plugins/pii-redaction/about.mdx @@ -24,7 +24,8 @@ The plugin supports these backend modes: - `builtin` - Uses a native Rust backend for deterministic payload sanitization. - `local_model` - - Reserves a future local-model backend lane for more stochastic detection behavior. + - Delegates bounded detector inference to a manifest-backed `grpc-v1` + worker while the PII component retains sanitization policy. ## Use This Plugin When @@ -39,6 +40,8 @@ Start here when you need to: first-party NeMo Relay components. - Use built-in detector presets for common values such as emails, phone numbers, URLs, API keys, and IP addresses without writing custom regexes. +- Compose deterministic recognizers with an optional local contextual + classifier without loading model dependencies into the Relay host. ## Plugin Versus Middleware @@ -90,18 +93,21 @@ The current built-in backend supports five actions: The current backend boundary is intentional: - Managed tool surfaces are sanitized as JSON payloads with exact JSON-pointer - targeting. + targeting or bounded single-segment wildcard patterns. - Managed LLM requests use the active resolved codec for each call, including built-in, runtime, and opaque codecs. Normalized response projection requires a recognized built-in codec because response codec capabilities are decode-only. This lets redaction target normalized Relay shapes such as - `/messages/0/content` and `/message`. + `/messages/*/content` and `/message`. - `mark` sanitizes `data`, `category_profile`, and `metadata` independently on every mark event. It defaults to `true`; set `mark = false` to opt out. - `input`, `output`, `tool_input`, and `tool_output` sanitize scope metadata on their corresponding lifecycle events. Tool and LLM primary data and typed profiles remain on their specialized sanitizer paths to avoid applying the same mask or hash twice. +- Local-model workers receive only the selected string values. Relay owns + batching, deadlines, confidence and label policy, response validation, + replacement, and fail-closed behavior. ## Observability Boundary @@ -116,20 +122,25 @@ That means: For managed LLM requests, codec decode and re-encode can canonicalize the emitted provider-shaped start event. For example, Relay can record an OpenAI Responses request in the codec's canonical `input` array form rather than the -original shorthand form. +original shorthand form. If codec processing fails, the local-model backend +replaces the entire emitted LLM body rather than applying normalized selectors +to an incompatible raw provider shape. ## Current Boundaries This plugin is intentionally scoped to a deterministic built-in backend plus a -future local-model extension point. +local worker-provider extension point. In particular: -- `local_model` is an extension point, not a complete backend implementation - today. +- Local-model providers are optional dynamic plugins. They register a + language-neutral JSON request-response contract through the `grpc-v1` + worker protocol. - The plugin does not mutate the real callback arguments or return values. -- The plugin does not add a subtree or prefix selector language beyond exact - JSON-pointer matching. +- `target_path_patterns` supports `*` as one complete JSON-pointer path + segment. It does not support recursive or partial-segment matching. +- `allow_network = false` prohibits network inference by contract, but the + worker process is not a network sandbox. Install only trusted providers. ## Pages diff --git a/docs/configure-plugins/pii-redaction/configuration.mdx b/docs/configure-plugins/pii-redaction/configuration.mdx index d10f84813..51979e6e0 100644 --- a/docs/configure-plugins/pii-redaction/configuration.mdx +++ b/docs/configure-plugins/pii-redaction/configuration.mdx @@ -128,14 +128,14 @@ The following table compares the available PII redaction backends: | Area | `builtin` | `local_model` | | --- | --- | --- | | Built-in component kind and config validation | Supported | Supported | -| Managed LLM `input` | Supported | Not implemented | -| Managed LLM `output` | Supported | Not implemented | -| Mark and generic scope event fields | Supported | Not implemented | -| Managed `tool_input` | Supported | Not implemented | -| Managed `tool_output` | Supported | Not implemented | +| Managed LLM `input` | Supported | Supported | +| Managed LLM `output` | Supported | Supported | +| Mark and generic scope event fields | Supported | Supported | +| Managed `tool_input` | Supported | Supported | +| Managed `tool_output` | Supported | Supported | | Built-in actions | `remove`, `redact`, `regex_replace`, `hash`, `mask` | N/A | -| Codec support | `openai_chat`, `openai_responses`, `anthropic_messages` | Runtime-specific future implementation | -| Runtime availability | Any runtime that includes the `nemo-relay-pii-redaction` plugin crate | Runtimes that install a local backend provider | +| Codec support | `openai_chat`, `openai_responses`, `anthropic_messages` | `openai_chat`, `openai_responses`, `anthropic_messages` | +| Runtime availability | Any runtime that includes the `nemo-relay-pii-redaction` plugin crate | Runtimes with an active `grpc-v1` worker provider | ## Built-in Mode @@ -217,7 +217,8 @@ Use the editor when you want to: - Set the LLM `codec` - Edit `builtin` action settings such as `action`, `target_paths`, `pattern`, `detector`, `replacement`, and masking fields -- Edit `local.backend` for a runtime-provided future local-model backend +- Edit worker-backed local-model settings such as `local.backend`, + `target_paths`, `target_path_patterns`, `min_score`, and `excluded_labels` The editor preserves unknown fields when it rewrites an existing `pii_redaction` component, so future or runtime-specific settings are not @@ -314,9 +315,117 @@ When `detector` is set and you do not specify `unmasked_prefix` or - `gcp_api_key`: Preserves the `AIza`-style prefix and the last four characters - `azure_storage_account_key`: Preserves the last four characters +## Local-Model Mode + +Use `local_model` when a manifest-backed `grpc-v1` worker should detect +contextual PII. The worker performs inference only. The PII component selects +fields, batches text, enforces deadlines, validates detections, applies +confidence and label policy, replaces accepted spans, and fails closed. + +The provider name is `/`. For example, a worker +with plugin ID `acme.pii_worker` that registers `detector` is selected as +`acme.pii_worker/detector`. + +```toml +[[components]] +kind = "pii_redaction" +enabled = true + +[components.config] +mode = "local_model" +codec = "openai_chat" + +[components.config.local] +backend = "acme.pii_worker/detector" +model_id = "acme-pii-v1" +detector_profile = "default" +min_score = 0.4 +excluded_labels = ["ORGANIZATION"] +target_path_patterns = [ + "/messages/*/content", + "/messages/*/content/*/text", + "/message", + "/message/*/text", +] +replacement = "[REDACTED]" +allow_network = false +max_latency_ms = 250 +``` + +Worker providers are installed before built-in components initialize and are +removed after their dependent sanitizers. `allow_network = true` is rejected: +this lane is for same-machine inference. This is a configuration contract, not +a process sandbox. + +Provider failures, timeouts, malformed responses, invalid UTF-8 spans, +overlapping spans, and input-limit violations fail closed for the affected +batch. If a configured codec cannot decode or safely re-encode an LLM payload, +Relay replaces the entire emitted request or response body. The default +deadline is 250 ms for the complete selected payload, including every provider +batch. Configuration above 60 seconds is rejected. + +Use `profiles` to run deterministic recognizers before a contextual model: + +```toml +[components.config] +codec = "openai_chat" + +[[components.config.profiles]] +mode = "builtin" +priority = 80 + +[components.config.profiles.builtin] +action = "redact" +detector = "email" + +[[components.config.profiles]] +mode = "local_model" +priority = 90 + +[components.config.profiles.local] +backend = "nemo_relay.pii_rampart/detector" +min_score = 0.4 +max_latency_ms = 1500 +target_path_patterns = [ + "/messages/*/content", + "/messages/*/content/*/text", + "/message", + "/message/*/text", +] +``` + +Keep contextual classifiers limited to normalized content paths. Sending model +names, tool IDs, trace IDs, routing fields, or arbitrary provider metadata to +a classifier increases false positives and exposes data outside the intended +policy boundary. + +The generic local-model payload deadline defaults to 250 ms. Contextual models +can need more time for large selected payloads. The Rampart profile above uses +1500 ms so a request near the 64 KiB provider limit has practical headroom; +benchmark representative inputs on deployment hardware before lowering it. + +The optional Rampart provider is distributed as a manifest-backed Python source +bundle under `crates/pii-redaction/providers/rampart`. Prefetch its pinned model +before adding and enabling the worker: + +```bash +cd crates/pii-redaction/providers/rampart +uvx --from . nemo-relay-pii-rampart-prefetch +nemo-relay plugins add ./relay-plugin.toml +nemo-relay plugins enable nemo_relay.pii_rampart +``` + +Activation remains offline-only. The prefetch command and activation both +verify the pinned model files before ONNX Runtime loads them. + ## Path Semantics -`target_paths` are exact JSON-pointer matches. +`target_paths` are exact JSON-pointer matches. `target_path_patterns` also +accepts `*` as one complete path segment. It does not provide recursive or +partial-segment matching. When both lists are empty, the selected backend +inspects every string leaf and reports a configuration warning. Use explicit +content paths for contextual local models so identifiers and provider metadata +do not enter the classifier accidentally. The plugin uses different payload boundaries for tools and LLMs: @@ -326,7 +435,8 @@ The plugin uses different payload boundaries for tools and LLMs: responses use the active built-in codec for normalized projection. Prefer normalized Relay paths such as: - `/headers/authorization` for an exact request header field - - `/messages/0/content` for request message content + - `/messages/*/content` for request message content + - `/messages/*/content/*/text` for multimodal request text - `/message` for the normalized assistant response text - Marks and non-tool, non-LLM scopes sanitize `data`, `category_profile`, and `metadata` as separate JSON values. A target path is evaluated independently @@ -406,23 +516,3 @@ For tool and LLM scope events, the PII redaction scope sanitizer leaves `data`, LLM sanitizer already owns that lifecycle boundary. This prevents repeated hashing or masking. Other scope categories use `input` for starts and `output` for ends. - -## Local Model Mode - -`local_model` is reserved for a future in-process local-model backend. - -### Current Status - -The current local-model status is: - -- The plugin contract accepts `mode = "local_model"`. -- The `local` section supports: - - `backend` - - `model_id` - - `detector_profile` - - `allow_network` - - `max_latency_ms` -- Actual behavior depends on a runtime-installed local backend provider. - -Without a provider, runtimes report the local backend as unavailable during -plugin initialization. diff --git a/go/nemo_relay/pii_redaction.go b/go/nemo_relay/pii_redaction.go index 871a83b60..43693abec 100644 --- a/go/nemo_relay/pii_redaction.go +++ b/go/nemo_relay/pii_redaction.go @@ -3,6 +3,8 @@ package nemo_relay +import "encoding/json" + // PiiRedactionPluginKind is the top-level plugin kind used by the built-in PII redaction component. const PiiRedactionPluginKind = "pii_redaction" @@ -18,13 +20,27 @@ type PiiRedactionBuiltinConfig struct { UnmaskedSuffix *int32 `json:"unmasked_suffix,omitempty"` } -// PiiRedactionLocalModelConfig configures the future local-model redaction backend. +// PiiRedactionLocalModelConfig configures a worker-backed local-model redaction provider. type PiiRedactionLocalModelConfig struct { - Backend string `json:"backend,omitempty"` - ModelID string `json:"model_id,omitempty"` - DetectorProfile string `json:"detector_profile,omitempty"` - AllowNetwork *bool `json:"allow_network,omitempty"` - MaxLatencyMS *int32 `json:"max_latency_ms,omitempty"` + Backend string `json:"backend,omitempty"` + ModelID string `json:"model_id,omitempty"` + DetectorProfile string `json:"detector_profile,omitempty"` + TargetPaths []string `json:"target_paths,omitempty"` + TargetPathPatterns []string `json:"target_path_patterns,omitempty"` + MinScore *float64 `json:"min_score,omitempty"` + ExcludedLabels []string `json:"excluded_labels,omitempty"` + Replacement *string `json:"replacement,omitempty"` + AllowNetwork *bool `json:"allow_network,omitempty"` + MaxLatencyMS *int32 `json:"max_latency_ms,omitempty"` +} + +// PiiRedactionProfile configures one ordered PII redaction backend. +type PiiRedactionProfile struct { + Enabled bool `json:"enabled"` + Mode string `json:"mode"` + Priority int32 `json:"priority"` + Builtin *PiiRedactionBuiltinConfig `json:"builtin,omitempty"` + Local *PiiRedactionLocalModelConfig `json:"local,omitempty"` } // PiiRedactionConfig is the canonical Go shape for the PII redaction plugin config document. @@ -38,11 +54,31 @@ type PiiRedactionConfig struct { ToolOutput bool `json:"tool_output"` Priority int32 `json:"priority,omitempty"` Codec string `json:"codec,omitempty"` + Profiles []PiiRedactionProfile `json:"profiles,omitempty"` Builtin *PiiRedactionBuiltinConfig `json:"builtin,omitempty"` Local *PiiRedactionLocalModelConfig `json:"local,omitempty"` Policy *ConfigPolicy `json:"policy,omitempty"` } +// MarshalJSON omits legacy top-level fields when profile composition is used. +func (config PiiRedactionConfig) MarshalJSON() ([]byte, error) { + if len(config.Profiles) == 0 { + type configAlias PiiRedactionConfig + return json.Marshal(configAlias(config)) + } + return json.Marshal(struct { + Version uint32 `json:"version,omitempty"` + Codec string `json:"codec,omitempty"` + Profiles []PiiRedactionProfile `json:"profiles"` + Policy *ConfigPolicy `json:"policy,omitempty"` + }{ + Version: config.Version, + Codec: config.Codec, + Profiles: config.Profiles, + Policy: config.Policy, + }) +} + // PiiRedactionComponentSpec wraps one PII redaction config as a top-level plugin component. type PiiRedactionComponentSpec struct { Enabled bool `json:"enabled,omitempty"` @@ -78,6 +114,15 @@ func NewPiiRedactionLocalModelConfig() PiiRedactionLocalModelConfig { return PiiRedactionLocalModelConfig{} } +// NewPiiRedactionProfile returns one enabled built-in profile with default priority. +func NewPiiRedactionProfile() PiiRedactionProfile { + return PiiRedactionProfile{ + Enabled: true, + Mode: "builtin", + Priority: 100, + } +} + // NewPiiRedactionComponentSpec wraps PII redaction config as an enabled component. func NewPiiRedactionComponentSpec(config PiiRedactionConfig) PiiRedactionComponentSpec { return PiiRedactionComponentSpec{ diff --git a/go/nemo_relay/pii_redaction/pii_redaction.go b/go/nemo_relay/pii_redaction/pii_redaction.go index 5a127ed96..68297995a 100644 --- a/go/nemo_relay/pii_redaction/pii_redaction.go +++ b/go/nemo_relay/pii_redaction/pii_redaction.go @@ -11,9 +11,12 @@ type Config = nemo_relay.PiiRedactionConfig // BuiltinConfig configures deterministic built-in redaction. type BuiltinConfig = nemo_relay.PiiRedactionBuiltinConfig -// LocalModelConfig configures the future local-model redaction backend. +// LocalModelConfig configures a worker-backed local-model redaction provider. type LocalModelConfig = nemo_relay.PiiRedactionLocalModelConfig +// Profile configures one ordered PII redaction backend. +type Profile = nemo_relay.PiiRedactionProfile + // ComponentSpec wraps PII redaction config as a top-level plugin component. type ComponentSpec = nemo_relay.PiiRedactionComponentSpec @@ -41,6 +44,11 @@ func NewLocalModelConfig() LocalModelConfig { return nemo_relay.NewPiiRedactionLocalModelConfig() } +// NewProfile returns one enabled built-in profile with default priority. +func NewProfile() Profile { + return nemo_relay.NewPiiRedactionProfile() +} + // NewComponentSpec wraps PII redaction config as an enabled component. func NewComponentSpec(config Config) ComponentSpec { return nemo_relay.NewPiiRedactionComponentSpec(config) diff --git a/go/nemo_relay/pii_redaction/pii_redaction_test.go b/go/nemo_relay/pii_redaction/pii_redaction_test.go index 3bb3301a2..dc78be99f 100644 --- a/go/nemo_relay/pii_redaction/pii_redaction_test.go +++ b/go/nemo_relay/pii_redaction/pii_redaction_test.go @@ -27,13 +27,23 @@ func TestPiiRedactionShorthandHelpers(t *testing.T) { func TestPiiRedactionComponentSpecAndLocalModelHelpers(t *testing.T) { config := NewConfig() local := NewLocalModelConfig() - local.Backend = "local" + minScore := 0.75 + local.Backend = "nemo_relay.pii_rampart/detector" local.ModelID = "pii-model" - config.Mode = "local" + local.TargetPathPatterns = []string{"/messages/*/content"} + local.MinScore = &minScore + local.ExcludedLabels = []string{"CITY"} + config.Mode = "local_model" config.Local = &local spec := NewComponentSpec(config) - if !spec.Enabled || spec.Config.Local == nil || spec.Config.Local.ModelID != "pii-model" { + if !spec.Enabled || + spec.Config.Local == nil || + spec.Config.Local.ModelID != "pii-model" || + len(spec.Config.Local.TargetPathPatterns) != 1 || + spec.Config.Local.MinScore == nil || + *spec.Config.Local.MinScore != minScore || + len(spec.Config.Local.ExcludedLabels) != 1 { t.Fatalf("unexpected PII redaction component spec: %#v", spec) } } diff --git a/go/nemo_relay/pii_redaction_test.go b/go/nemo_relay/pii_redaction_test.go index 5b5728adc..d9591fbed 100644 --- a/go/nemo_relay/pii_redaction_test.go +++ b/go/nemo_relay/pii_redaction_test.go @@ -3,7 +3,11 @@ package nemo_relay -import "testing" +import ( + "encoding/json" + "reflect" + "testing" +) func TestPiiRedactionConfigHelpers(t *testing.T) { config := NewPiiRedactionConfig() @@ -18,9 +22,13 @@ func TestPiiRedactionConfigHelpers(t *testing.T) { t.Fatalf("unexpected built-in redaction defaults: %#v", builtin) } local := NewPiiRedactionLocalModelConfig() - if local != (PiiRedactionLocalModelConfig{}) { + if !reflect.DeepEqual(local, PiiRedactionLocalModelConfig{}) { t.Fatalf("unexpected local model defaults: %#v", local) } + profile := NewPiiRedactionProfile() + if !profile.Enabled || profile.Mode != "builtin" || profile.Priority != 100 { + t.Fatalf("unexpected profile defaults: %#v", profile) + } config.Builtin = &builtin component := PiiRedactionComponent(config) @@ -42,6 +50,30 @@ func TestPiiRedactionConfigHelpers(t *testing.T) { } } +func TestPiiRedactionProfilesOmitLegacyTopLevelFields(t *testing.T) { + config := NewPiiRedactionConfig() + config.Profiles = []PiiRedactionProfile{ + NewPiiRedactionProfile(), + } + serialized, err := json.Marshal(config) + if err != nil { + t.Fatalf("marshal profile config: %v", err) + } + var value map[string]any + if err := json.Unmarshal(serialized, &value); err != nil { + t.Fatalf("decode profile config: %v", err) + } + if _, present := value["mode"]; present { + t.Fatalf("profile config retained legacy mode: %#v", value) + } + if _, present := value["input"]; present { + t.Fatalf("profile config retained legacy input: %#v", value) + } + if len(value["profiles"].([]any)) != 1 { + t.Fatalf("unexpected profile config: %#v", value) + } +} + func TestPiiRedactionValidationRejectsBadValues(t *testing.T) { config := NewPiiRedactionConfig() config.Input = false diff --git a/justfile b/justfile index c64a671ab..1819fad5c 100644 --- a/justfile +++ b/justfile @@ -971,6 +971,8 @@ check-python-worker-proto: } assert pb.SUBSCRIBER == 1 assert pb.LLM_STREAM_EXECUTION_INTERCEPT == 25 + assert pb.LOCAL_MODEL_PROVIDER == 40 + assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["provider"].number == 13 PY generate-worker-plugin-lockfile: diff --git a/python/nemo_relay/pii_redaction.py b/python/nemo_relay/pii_redaction.py index 53c2b77f8..de4f443d0 100644 --- a/python/nemo_relay/pii_redaction.py +++ b/python/nemo_relay/pii_redaction.py @@ -103,11 +103,16 @@ def to_dict(self) -> JsonObject: @dataclass(slots=True) class LocalModelConfig: - """Future local-model backend seam settings.""" + """Worker-backed local-model redaction settings.""" backend: str | None = None model_id: str | None = None detector_profile: str | None = None + target_paths: list[str] = field(default_factory=list) + target_path_patterns: list[str] = field(default_factory=list) + min_score: float | None = None + excluded_labels: list[str] = field(default_factory=list) + replacement: str | None = None allow_network: bool | None = None max_latency_ms: int | None = None @@ -118,12 +123,40 @@ def to_dict(self) -> JsonObject: "backend": self.backend, "model_id": self.model_id, "detector_profile": self.detector_profile, + "target_paths": self.target_paths or None, + "target_path_patterns": self.target_path_patterns or None, + "min_score": self.min_score, + "excluded_labels": self.excluded_labels or None, + "replacement": self.replacement, "allow_network": self.allow_network, "max_latency_ms": self.max_latency_ms, } ) +@dataclass(slots=True) +class PiiRedactionProfile: + """One ordered PII redaction backend profile.""" + + enabled: bool = True + mode: Literal["builtin", "local_model"] = "builtin" + priority: int = 100 + builtin: BuiltinConfig | None = None + local: LocalModelConfig | None = None + + def to_dict(self) -> JsonObject: + """Serialize this profile to the canonical JSON object shape.""" + return _normalize_object( + { + "enabled": self.enabled, + "mode": self.mode, + "priority": self.priority, + "builtin": self.builtin, + "local": self.local, + } + ) + + @dataclass(slots=True) class PiiRedactionConfig: """Canonical config document for the top-level PII redaction component.""" @@ -137,12 +170,22 @@ class PiiRedactionConfig: mark: bool = True priority: int = 100 codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = None + profiles: list[PiiRedactionProfile] = field(default_factory=list) builtin: BuiltinConfig | None = None local: LocalModelConfig | None = None policy: ConfigPolicy = field(default_factory=ConfigPolicy) def to_dict(self) -> JsonObject: """Serialize this PII redaction config to the canonical JSON object shape.""" + if self.profiles: + return _normalize_object( + { + "version": self.version, + "codec": self.codec, + "profiles": self.profiles, + "policy": self.policy, + } + ) return _normalize_object( { "version": self.version, @@ -187,7 +230,7 @@ def validate_config(config: PiiRedactionConfig | JsonObject) -> ConfigReport: components=[ComponentSpec(config)], ) ) - return cast(ConfigReport, report) + return report __all__ = [ @@ -199,5 +242,6 @@ def validate_config(config: PiiRedactionConfig | JsonObject) -> ConfigReport: "LocalModelConfig", "PII_REDACTION_PLUGIN_KIND", "PiiRedactionConfig", + "PiiRedactionProfile", "validate_config", ] diff --git a/python/nemo_relay/pii_redaction.pyi b/python/nemo_relay/pii_redaction.pyi index 244f6a3ef..fd6abaa72 100644 --- a/python/nemo_relay/pii_redaction.pyi +++ b/python/nemo_relay/pii_redaction.pyi @@ -44,10 +44,24 @@ class LocalModelConfig: backend: str | None = ... model_id: str | None = ... detector_profile: str | None = ... + target_paths: list[str] = field(default_factory=list) + target_path_patterns: list[str] = field(default_factory=list) + min_score: float | None = ... + excluded_labels: list[str] = field(default_factory=list) + replacement: str | None = ... allow_network: bool | None = ... max_latency_ms: int | None = ... def to_dict(self) -> JsonObject: ... +@dataclass(slots=True) +class PiiRedactionProfile: + enabled: bool = ... + mode: Literal["builtin", "local_model"] = ... + priority: int = ... + builtin: BuiltinConfig | None = ... + local: LocalModelConfig | None = ... + def to_dict(self) -> JsonObject: ... + @dataclass(slots=True) class PiiRedactionConfig: version: int = ... @@ -59,6 +73,7 @@ class PiiRedactionConfig: mark: bool = ... priority: int = ... codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = ... + profiles: list[PiiRedactionProfile] = field(default_factory=list) builtin: BuiltinConfig | None = ... local: LocalModelConfig | None = ... policy: ConfigPolicy = field(default_factory=ConfigPolicy) diff --git a/python/tests/test_pii_redaction_plugin.py b/python/tests/test_pii_redaction_plugin.py index cbed8c4c2..1e0debc8b 100644 --- a/python/tests/test_pii_redaction_plugin.py +++ b/python/tests/test_pii_redaction_plugin.py @@ -13,6 +13,7 @@ ConfigPolicy, LocalModelConfig, PiiRedactionConfig, + PiiRedactionProfile, validate_config, ) @@ -29,6 +30,25 @@ def test_defaults_and_component_wrapper(self): "unsupported_value": "error", } assert LocalModelConfig().to_dict() == {} + assert LocalModelConfig( + backend="acme.pii/detector", + target_paths=["/message"], + target_path_patterns=["/messages/*/content"], + min_score=0.6, + excluded_labels=["CITY"], + replacement="[PRIVATE]", + allow_network=False, + max_latency_ms=250, + ).to_dict() == { + "backend": "acme.pii/detector", + "target_paths": ["/message"], + "target_path_patterns": ["/messages/*/content"], + "min_score": 0.6, + "excluded_labels": ["CITY"], + "replacement": "[PRIVATE]", + "allow_network": False, + "max_latency_ms": 250, + } wrapped = ComponentSpec(PiiRedactionConfig()).to_dict() assert wrapped["kind"] == PII_REDACTION_PLUGIN_KIND @@ -42,6 +62,36 @@ def test_defaults_and_component_wrapper(self): opted_out = PiiRedactionConfig(mark=False).to_dict() assert opted_out["mark"] is False + def test_profile_config_omits_legacy_top_level_fields(self): + config = PiiRedactionConfig( + codec="openai_chat", + profiles=[ + PiiRedactionProfile( + mode="builtin", + builtin=BuiltinConfig(detector="email"), + ), + PiiRedactionProfile( + mode="local_model", + priority=110, + local=LocalModelConfig( + backend="acme.pii/detector", + target_path_patterns=["/messages/*/content"], + ), + ), + ], + ).to_dict() + + profiles = config["profiles"] + assert isinstance(profiles, list) + local_profile = profiles[1] + assert isinstance(local_profile, dict) + local = local_profile["local"] + assert isinstance(local, dict) + assert local["backend"] == "acme.pii/detector" + assert "mode" not in config + assert "input" not in config + assert validate_config(config)["diagnostics"] == [] + def test_validation_rejects_bad_values(self): report = validate_config( PiiRedactionConfig( From 9e103444c6bb3b84c82ae697439358d2e4e11eba Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sat, 25 Jul 2026 18:20:17 -0700 Subject: [PATCH 03/83] feat(pii): add optional Rampart worker provider Signed-off-by: Alex Fournier --- .../providers/rampart/MANIFEST.in | 5 + .../pii-redaction/providers/rampart/README.md | 138 +++++ .../providers/rampart/THIRD_PARTY_NOTICES.md | 18 + .../providers/rampart/config.schema.json | 53 ++ .../nemo_relay_pii_rampart/__init__.py | 13 + .../nemo_relay_pii_rampart/detector.py | 553 ++++++++++++++++++ .../nemo_relay_pii_rampart/prefetch.py | 27 + .../rampart/nemo_relay_pii_rampart/py.typed | 0 .../rampart/nemo_relay_pii_rampart/worker.py | 99 ++++ .../providers/rampart/pyproject.toml | 57 ++ .../providers/rampart/relay-plugin.toml | 32 + .../providers/rampart/tests/test_detector.py | 276 +++++++++ .../providers/rampart/tests/test_worker.py | 139 +++++ .../tests/worker_provider_tests.rs | 352 +++++++++++ 14 files changed, 1762 insertions(+) create mode 100644 crates/pii-redaction/providers/rampart/MANIFEST.in create mode 100644 crates/pii-redaction/providers/rampart/README.md create mode 100644 crates/pii-redaction/providers/rampart/THIRD_PARTY_NOTICES.md create mode 100644 crates/pii-redaction/providers/rampart/config.schema.json create mode 100644 crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py create mode 100644 crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py create mode 100644 crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/prefetch.py create mode 100644 crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/py.typed create mode 100644 crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py create mode 100644 crates/pii-redaction/providers/rampart/pyproject.toml create mode 100644 crates/pii-redaction/providers/rampart/relay-plugin.toml create mode 100644 crates/pii-redaction/providers/rampart/tests/test_detector.py create mode 100644 crates/pii-redaction/providers/rampart/tests/test_worker.py create mode 100644 crates/pii-redaction/tests/worker_provider_tests.rs diff --git a/crates/pii-redaction/providers/rampart/MANIFEST.in b/crates/pii-redaction/providers/rampart/MANIFEST.in new file mode 100644 index 000000000..af375315e --- /dev/null +++ b/crates/pii-redaction/providers/rampart/MANIFEST.in @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +include config.schema.json +include relay-plugin.toml diff --git a/crates/pii-redaction/providers/rampart/README.md b/crates/pii-redaction/providers/rampart/README.md new file mode 100644 index 000000000..ec8b811c1 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/README.md @@ -0,0 +1,138 @@ + + +# Rampart PII Provider + +This optional manifest-backed Python worker runs the +[`nationaldesignstudio/rampart`](https://huggingface.co/nationaldesignstudio/rampart) +ONNX token classifier behind NeMo Relay's `pii_redaction.local_model` backend. +Relay owns field selection, event sanitization, replacement, deadlines, and +fail-closed behavior. The worker performs detector inference only. + +The worker runs as a local child process over Relay's `grpc-v1` protocol. Its +Python, ONNX Runtime, NumPy, tokenizer, and model-cache dependencies remain in a +Relay-managed virtual environment rather than the Relay host process. Process +isolation is not a security sandbox. + +## Install + +From this directory: + +```bash +uvx --from . nemo-relay-pii-rampart-prefetch +nemo-relay plugins add ./relay-plugin.toml +nemo-relay plugins enable nemo_relay.pii_rampart +``` + +If the provider package is already installed, run +`nemo-relay-pii-rampart-prefetch` directly. `plugins add` creates a separate +Relay-managed Python environment from the same source directory. + +Rampart activation is offline-only. It requires the pinned 14.7 MB model +snapshot to already exist in the Hugging Face cache. Model acquisition is an +explicit setup step and never occurs during activation or inference. + +`model_id` normally remains `nationaldesignstudio/rampart`. The worker rejects +other repository identifiers and revisions because its integrity manifest pins +one supported snapshot. To load that same snapshot from a local directory, set +`model_id` to an absolute path or use explicit `./`, `../`, or `~/` syntax so a +relative directory cannot shadow the repository identifier. The PII +component's optional `local.model_id` remains the logical +`nationaldesignstudio/rampart` identifier even when worker storage uses a local +path. + +On hosts with slow or restricted access to Hugging Face, populate the shared +cache before enabling the plugin: + +```bash +nemo-relay-pii-rampart-prefetch +``` + +Run that command as the same operating-system user that runs Relay. If +`cache_dir` is configured for the plugin, pass the same value with +`--cache-dir`. The prefetch command and activation both verify SHA-256 digests +for every runtime model and tokenizer file. A missing or modified file blocks +activation. + +## Configure + +Add worker settings to the `[[plugins.dynamic]]` record created by `plugins +add`: + +```toml +[plugins.dynamic.config] +local_files_only = true +max_windows_per_request = 128 +inference_batch_size = 16 +max_pending_requests = 8 +``` + +Add the PII component to the same `plugins.toml`: + +```toml +[[components]] +kind = "pii_redaction" +enabled = true + +[components.config] +mode = "local_model" +codec = "openai_chat" + +[components.config.local] +backend = "nemo_relay.pii_rampart/detector" +model_id = "nationaldesignstudio/rampart" +detector_profile = "default" +allow_network = false +max_latency_ms = 1500 +min_score = 0.4 +replacement = "[REDACTED]" +target_path_patterns = [ + "/messages/*/content", + "/messages/*/content/*/text", + "/message", + "/message/*/text", +] +``` + +`allow_network = false` means provider inference is local. It does not sandbox +the worker. `local_files_only` must remain `true`; activation-time model +acquisition is not supported. + +Rampart is the contextual detector lane, not a replacement for deterministic +recognizers. Configure built-in PII profiles for structured values and the +local-model profile for names and contextual identifiers. Keep the local-model +profile limited to normalized content paths. Classifying every string leaf can +produce false positives on model names, region names, UUIDs, trace IDs, and +other machine identifiers. Relay, not the worker, applies `min_score` and +optional `excluded_labels` policy after validating the provider response. + +## Runtime Bounds + +- At most 64 texts and 64 KiB of UTF-8 text are accepted per provider request. +- Each text is limited to 16 KiB. +- Long inputs use overlapping 510-token windows, with 64 content tokens of + overlap. +- ONNX inference batches and total windows are bounded by worker configuration. +- Requests above the provider bounds return an error; the PII component then + fails closed for the affected batch. +- `max_latency_ms` is one total budget for all provider batches selected from + one payload. +- CPU inference is serialized per worker process. Host deadlines cancel the + RPC, while already-running native inference is allowed to finish before its + admission slot is released. +- Use a `max_latency_ms` of at least 1500 when the selected payload can approach + the 64 KiB provider-request limit. Smaller content-only payloads normally + complete much faster. Benchmark representative inputs on deployment + hardware before lowering the deadline. + +The default model supports English, Spanish, French, German, Italian, +Portuguese, and Dutch. Its model card documents weak recall for non-Latin +scripts and government identifiers. Do not treat this detector as a complete +security boundary. + +## Attribution + +See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md). The model is downloaded +only by the explicit prefetch command and is not redistributed by this package. diff --git a/crates/pii-redaction/providers/rampart/THIRD_PARTY_NOTICES.md b/crates/pii-redaction/providers/rampart/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..248eef265 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/THIRD_PARTY_NOTICES.md @@ -0,0 +1,18 @@ + + +# Third-Party Notices + +This optional provider downloads and executes **Rampart**, published by +National Design Studio at +[`nationaldesignstudio/rampart`](https://huggingface.co/nationaldesignstudio/rampart). +The model and its training-data attribution are published under the +[Creative Commons Attribution 4.0 International +license](https://creativecommons.org/licenses/by/4.0/). + +The default provider configuration selects model revision +`b1993e4e68b082835b80ffc65acc03325ea2e501`. Model files are downloaded to the +operator's Hugging Face cache and are not distributed in the NeMo Relay source +or Python package. diff --git a/crates/pii-redaction/providers/rampart/config.schema.json b/crates/pii-redaction/providers/rampart/config.schema.json new file mode 100644 index 000000000..769119073 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/config.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "NeMo Relay Rampart PII Provider", + "type": "object", + "additionalProperties": false, + "properties": { + "model_id": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "default": "nationaldesignstudio/rampart", + "description": "Pinned Rampart repository identifier or an explicit local directory containing the same verified snapshot." + }, + "revision": { + "type": "string", + "const": "b1993e4e68b082835b80ffc65acc03325ea2e501", + "default": "b1993e4e68b082835b80ffc65acc03325ea2e501" + }, + "cache_dir": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "local_files_only": { + "type": "boolean", + "const": true, + "default": true + }, + "max_windows_per_request": { + "type": "integer", + "minimum": 1, + "maximum": 512, + "default": 128 + }, + "inference_batch_size": { + "type": "integer", + "minimum": 1, + "maximum": 64, + "default": 16 + }, + "max_pending_requests": { + "type": "integer", + "minimum": 1, + "maximum": 64, + "default": 8 + }, + "intra_op_threads": { + "type": "integer", + "minimum": 1, + "maximum": 64 + } + } +} diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py new file mode 100644 index 000000000..71f7c3e97 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Rampart local-model provider for the NeMo Relay PII component.""" + +from .detector import DEFAULT_MODEL_ID, DEFAULT_MODEL_REVISION, RampartDetector, RampartSettings + +__all__ = [ + "DEFAULT_MODEL_ID", + "DEFAULT_MODEL_REVISION", + "RampartDetector", + "RampartSettings", +] diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py new file mode 100644 index 000000000..8f8844229 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py @@ -0,0 +1,553 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Bounded ONNX token-classification adapter for the Rampart PII model.""" + +from __future__ import annotations + +import hashlib +import json +import threading +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Protocol + +import numpy as np # ty: ignore[unresolved-import] +import onnxruntime as ort # ty: ignore[unresolved-import] +from huggingface_hub import snapshot_download # ty: ignore[unresolved-import] +from tokenizers import Tokenizer # ty: ignore[unresolved-import] + +DEFAULT_MODEL_ID = "nationaldesignstudio/rampart" +DEFAULT_MODEL_REVISION = "b1993e4e68b082835b80ffc65acc03325ea2e501" +CONTRACT_VERSION = 1 +MAX_TEXTS_PER_REQUEST = 64 +MAX_TEXT_BYTES = 16 * 1024 +MAX_REQUEST_TEXT_BYTES = 64 * 1024 +MODEL_MAX_TOKENS = 512 +SPECIAL_TOKEN_COUNT = 2 +CONTENT_TOKEN_BUDGET = MODEL_MAX_TOKENS - SPECIAL_TOKEN_COUNT +WINDOW_OVERLAP_TOKENS = 64 +MAX_MODEL_REFERENCE_BYTES = 1024 +MAX_CACHE_PATH_BYTES = 4096 + +_MODEL_FILE_SHA256 = { + "config.json": "003b84bbcd489f5e782fe5cad8f3249c3653ec880089abb1ccc398a0d895e3e6", + "onnx/model_q4.onnx": "9f27d24949b0581701071ea5ef522d77ccd3f50c525cc91eac4d265b0fc2afe5", + "special_tokens_map.json": "5d5b662e421ea9fac075174bb0688ee0d9431699900b90662acd44b2a350503a", + "tokenizer.json": "98ade711428b42a1b5343c403a73344535e92de8e19359cdb567ef34da210259", + "tokenizer_config.json": "0088a6f8bcdd4014184fb068b83ebb12896a9db2bb269a71f73de83fef24bceb", +} + + +@dataclass(frozen=True) +class RampartSettings: + """Activation-time settings for one Rampart worker.""" + + model_id: str = DEFAULT_MODEL_ID + revision: str = DEFAULT_MODEL_REVISION + cache_dir: str | None = None + local_files_only: bool = True + max_windows_per_request: int = 128 + inference_batch_size: int = 16 + max_pending_requests: int = 8 + intra_op_threads: int | None = None + + @classmethod + def from_config(cls, config: Any) -> RampartSettings: + """Parse and validate dynamic-plugin configuration.""" + if not isinstance(config, dict): + raise TypeError("plugin config must be a JSON object") + allowed = { + "model_id", + "revision", + "cache_dir", + "local_files_only", + "max_windows_per_request", + "inference_batch_size", + "max_pending_requests", + "intra_op_threads", + } + unknown = sorted(set(config) - allowed) + if unknown: + raise ValueError(f"unknown plugin config field(s): {', '.join(unknown)}") + + model_id = _bounded_string( + config.get("model_id", DEFAULT_MODEL_ID), + "model_id", + MAX_MODEL_REFERENCE_BYTES, + ) + revision = _bounded_string( + config.get("revision", DEFAULT_MODEL_REVISION), + "revision", + MAX_MODEL_REFERENCE_BYTES, + ) + if not _uses_explicit_model_path(model_id) and model_id != DEFAULT_MODEL_ID: + raise ValueError(f"model_id must be {DEFAULT_MODEL_ID!r} or an explicit local directory") + if revision != DEFAULT_MODEL_REVISION: + raise ValueError(f"revision must be the pinned Rampart revision {DEFAULT_MODEL_REVISION!r}") + cache_dir = config.get("cache_dir") + if cache_dir is not None: + cache_dir = _bounded_string(cache_dir, "cache_dir", MAX_CACHE_PATH_BYTES) + local_files_only = config.get("local_files_only", True) + if not isinstance(local_files_only, bool): + raise TypeError("local_files_only must be a boolean") + if not local_files_only: + raise ValueError("local_files_only must remain true; prefetch the pinned model before enabling the plugin") + + return cls( + model_id=model_id, + revision=revision, + cache_dir=cache_dir, + local_files_only=local_files_only, + max_windows_per_request=_bounded_integer( + config.get("max_windows_per_request", 128), + "max_windows_per_request", + 1, + 512, + ), + inference_batch_size=_bounded_integer( + config.get("inference_batch_size", 16), + "inference_batch_size", + 1, + 64, + ), + max_pending_requests=_bounded_integer( + config.get("max_pending_requests", 8), + "max_pending_requests", + 1, + 64, + ), + intra_op_threads=_optional_bounded_integer(config.get("intra_op_threads"), "intra_op_threads", 1, 64), + ) + + +@dataclass(frozen=True) +class _InputText: + text_id: int + text: str + + +@dataclass(frozen=True) +class _Window: + text_id: int + input_ids: tuple[int, ...] + token_type_ids: tuple[int, ...] + offsets: tuple[tuple[int, int] | None, ...] + + +@dataclass(frozen=True) +class _Span: + start: int + end: int + label: str + score: float + + +class _Tokenizer(Protocol): + def encode(self, sequence: str, add_special_tokens: bool = True) -> Any: ... + + def token_to_id(self, token: str) -> int | None: ... + + +class _Session(Protocol): + def get_inputs(self) -> list[Any]: ... + + def get_outputs(self) -> list[Any]: ... + + def run(self, output_names: list[str], input_feed: dict[str, np.ndarray[Any, Any]]) -> list[Any]: ... + + +class RampartDetector: + """Load one Rampart model and perform bounded, serialized inference.""" + + def __init__( + self, + settings: RampartSettings, + tokenizer: _Tokenizer, + session: _Session, + labels: dict[int, str], + ) -> None: + self.settings = settings + self._tokenizer = tokenizer + self._session = session + self._labels = labels + if set(labels) != set(range(len(labels))): + raise ValueError("Rampart label IDs must be contiguous from zero") + self._lock = threading.Lock() + self._cls_id = _required_token_id(tokenizer, "[CLS]") + self._sep_id = _required_token_id(tokenizer, "[SEP]") + self._pad_id = _required_token_id(tokenizer, "[PAD]") + self._validate_model_contract() + + @classmethod + def load(cls, settings: RampartSettings) -> RampartDetector: + """Resolve model files and initialize an optimized CPU session.""" + model_root = resolve_verified_model_root(settings) + config = json.loads((model_root / "config.json").read_text(encoding="utf-8")) + raw_labels = config.get("id2label") + if not isinstance(raw_labels, dict): + raise ValueError("Rampart config.json must contain an id2label object") + labels = {int(index): str(label) for index, label in raw_labels.items()} + if not labels or labels.get(0) != "O": + raise ValueError("Rampart label map must define label 0 as O") + + options = ort.SessionOptions() + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + options.inter_op_num_threads = 1 + if settings.intra_op_threads is not None: + options.intra_op_num_threads = settings.intra_op_threads + session = ort.InferenceSession( + str(model_root / "onnx" / "model_q4.onnx"), + sess_options=options, + providers=["CPUExecutionProvider"], + ) + tokenizer = Tokenizer.from_file(str(model_root / "tokenizer.json")) + detector = cls(settings, tokenizer, session, labels) + detector._detect_texts([_InputText(0, "warmup")]) + return detector + + def detect_request(self, request: Any) -> dict[str, Any]: + """Validate one provider request and return versioned UTF-8 spans.""" + texts, requested_model = _parse_request(request) + if requested_model is not None and requested_model != DEFAULT_MODEL_ID: + raise ValueError(f"request model_id {requested_model!r} does not match loaded model {DEFAULT_MODEL_ID!r}") + profile = request.get("detector_profile") + if profile not in (None, "default"): + raise ValueError(f"unsupported detector_profile {profile!r}") + + with self._lock: + detections = self._detect_texts(texts) + return { + "version": CONTRACT_VERSION, + "detections": detections, + } + + def _validate_model_contract(self) -> None: + inputs = {entry.name for entry in self._session.get_inputs()} + expected_inputs = {"input_ids", "attention_mask", "token_type_ids"} + if inputs != expected_inputs: + raise ValueError(f"Rampart ONNX inputs must be {sorted(expected_inputs)}, got {sorted(inputs)}") + outputs = self._session.get_outputs() + if len(outputs) != 1 or outputs[0].name != "logits": + raise ValueError("Rampart ONNX model must expose one logits output") + + def _detect_texts(self, texts: list[_InputText]) -> list[dict[str, Any]]: + windows = self._build_windows(texts) + spans_by_text: dict[int, list[_Span]] = {item.text_id: [] for item in texts} + for start in range(0, len(windows), self.settings.inference_batch_size): + batch = windows[start : start + self.settings.inference_batch_size] + logits = self._infer(batch) + for window, window_logits in zip(batch, logits, strict=True): + spans_by_text[window.text_id].extend(self._decode_window(window, window_logits)) + + detections = [] + text_by_id = {item.text_id: item.text for item in texts} + byte_offsets_by_id = {text_id: _utf8_offsets(text) for text_id, text in text_by_id.items()} + for text_id, spans in spans_by_text.items(): + for span in _merge_overlapping_spans(spans): + byte_offsets = byte_offsets_by_id[text_id] + detections.append( + { + "text_id": text_id, + "start_utf8": byte_offsets[span.start], + "end_utf8": byte_offsets[span.end], + "label": span.label, + "score": span.score, + } + ) + return detections + + def _build_windows(self, texts: list[_InputText]) -> list[_Window]: + windows = [] + step = CONTENT_TOKEN_BUDGET - WINDOW_OVERLAP_TOKENS + for item in texts: + inference_text = item.text.replace("-", " ") + encoding = self._tokenizer.encode(inference_text, add_special_tokens=False) + ids = list(encoding.ids) + type_ids = list(encoding.type_ids) + offsets = list(encoding.offsets) + if not (len(ids) == len(type_ids) == len(offsets)): + raise ValueError("tokenizer returned inconsistent token metadata") + if any(start < 0 or end < start or end > len(inference_text) for start, end in offsets): + raise ValueError("tokenizer returned invalid character offsets") + for start in range(0, len(ids), step): + end = min(start + CONTENT_TOKEN_BUDGET, len(ids)) + windows.append( + _Window( + text_id=item.text_id, + input_ids=(self._cls_id, *ids[start:end], self._sep_id), + token_type_ids=(0, *type_ids[start:end], 0), + offsets=(None, *offsets[start:end], None), + ) + ) + if len(windows) > self.settings.max_windows_per_request: + raise ValueError( + f"request exceeded max_windows_per_request={self.settings.max_windows_per_request}" + ) + if end == len(ids): + break + return windows + + def _infer(self, windows: list[_Window]) -> np.ndarray[Any, np.dtype[np.float32]]: + max_length = max(len(window.input_ids) for window in windows) + shape = (len(windows), max_length) + input_ids = np.full(shape, self._pad_id, dtype=np.int64) + attention_mask = np.zeros(shape, dtype=np.int64) + token_type_ids = np.zeros(shape, dtype=np.int64) + for index, window in enumerate(windows): + length = len(window.input_ids) + input_ids[index, :length] = window.input_ids + attention_mask[index, :length] = 1 + token_type_ids[index, :length] = window.token_type_ids + result = self._session.run( + ["logits"], + { + "input_ids": input_ids, + "attention_mask": attention_mask, + "token_type_ids": token_type_ids, + }, + ) + logits = np.asarray(result[0], dtype=np.float32) + expected = (len(windows), max_length, len(self._labels)) + if logits.shape != expected: + raise ValueError(f"Rampart logits shape must be {expected}, got {logits.shape}") + if not np.isfinite(logits).all(): + raise ValueError("Rampart logits must contain only finite values") + return logits + + def _decode_window(self, window: _Window, logits: np.ndarray[Any, Any]) -> list[_Span]: + label_ids = np.argmax(logits, axis=-1) + maxima = np.max(logits, axis=-1) + scores = 1.0 / np.exp(logits - maxima[:, None]).sum(axis=-1) + spans = [] + current_label: str | None = None + current_start = 0 + current_end = 0 + current_score = 0.0 + current_count = 0 + + def finish() -> None: + nonlocal current_label, current_start, current_end, current_score, current_count + if current_label is not None: + score = current_score / current_count + spans.append(_Span(current_start, current_end, current_label, score)) + current_label = None + current_start = 0 + current_end = 0 + current_score = 0.0 + current_count = 0 + + for index, offset in enumerate(window.offsets): + if offset is None or offset[0] >= offset[1]: + finish() + continue + raw_label = self._labels.get(int(label_ids[index])) + score = float(scores[index]) + prefix, label = _split_bio_label(raw_label) + if label is None: + finish() + continue + if current_label is None or prefix == "B" or label != current_label: + finish() + current_label = label + current_start = offset[0] + current_end = offset[1] + current_score = score + current_count = 1 + else: + current_end = max(current_end, offset[1]) + current_score += score + current_count += 1 + finish() + return spans + + +def _resolve_model_root(settings: RampartSettings) -> Path: + local_path = _explicit_model_root(settings.model_id) + if local_path is not None: + return local_path + resolved = snapshot_download( + settings.model_id, + revision=settings.revision, + cache_dir=settings.cache_dir, + allow_patterns=list(_MODEL_FILE_SHA256), + local_files_only=True, + ) + return Path(resolved) + + +def resolve_verified_model_root(settings: RampartSettings) -> Path: + """Resolve and verify the pinned model assets without loading ONNX Runtime.""" + model_root = _resolve_model_root(settings) + _verify_model_files(model_root) + return model_root + + +def prefetch_verified_model(cache_dir: str | None = None) -> Path: + """Download and verify the pinned Rampart assets outside plugin activation.""" + if cache_dir is not None: + cache_dir = _bounded_string(cache_dir, "cache_dir", MAX_CACHE_PATH_BYTES) + model_root = Path( + snapshot_download( + DEFAULT_MODEL_ID, + revision=DEFAULT_MODEL_REVISION, + cache_dir=cache_dir, + allow_patterns=list(_MODEL_FILE_SHA256), + local_files_only=False, + ) + ) + _verify_model_files(model_root) + return model_root + + +def _verify_model_files( + model_root: Path, + expected: Mapping[str, str] = _MODEL_FILE_SHA256, +) -> None: + for relative_path, expected_sha256 in expected.items(): + path = model_root / relative_path + if not path.is_file(): + raise ValueError(f"Rampart model is missing required file {relative_path!r}") + with path.open("rb") as model_file: + digest = hashlib.file_digest(model_file, "sha256").hexdigest() + if digest != expected_sha256: + raise ValueError(f"Rampart model file {relative_path!r} failed SHA-256 verification") + + +def _explicit_model_root(model_id: str) -> Path | None: + candidate = Path(model_id).expanduser() + if not _uses_explicit_model_path(model_id): + return None + if not candidate.is_dir(): + raise ValueError(f"local Rampart model directory does not exist: {model_id}") + return candidate.resolve() + + +def _uses_explicit_model_path(model_id: str) -> bool: + return Path(model_id).expanduser().is_absolute() or model_id.startswith(("./", "../", ".\\", "..\\", "~/", "~\\")) + + +def _parse_request(request: Any) -> tuple[list[_InputText], str | None]: + if not isinstance(request, dict): + raise TypeError("local-model request must be a JSON object") + allowed = {"version", "model_id", "detector_profile", "texts"} + unknown = sorted(set(request) - allowed) + if unknown: + raise ValueError(f"unknown local-model request field(s): {', '.join(unknown)}") + version = request.get("version") + if isinstance(version, bool) or version != CONTRACT_VERSION: + raise ValueError(f"local-model request version must be {CONTRACT_VERSION}") + model_id = request.get("model_id") + if model_id is not None: + model_id = _nonempty_string(model_id, "model_id") + profile = request.get("detector_profile") + if profile is not None: + _nonempty_string(profile, "detector_profile") + raw_texts = request.get("texts") + if not isinstance(raw_texts, list) or not raw_texts: + raise TypeError("texts must be a non-empty array") + if len(raw_texts) > MAX_TEXTS_PER_REQUEST: + raise ValueError(f"texts must contain at most {MAX_TEXTS_PER_REQUEST} items") + + texts = [] + seen_ids = set() + total_bytes = 0 + for item in raw_texts: + if not isinstance(item, dict) or set(item) != {"id", "text"}: + raise TypeError("each texts item must contain exactly id and text") + text_id = item["id"] + text = item["text"] + if isinstance(text_id, bool) or not isinstance(text_id, int) or not 0 <= text_id <= 2**32 - 1: + raise TypeError("text id must be an unsigned 32-bit integer") + if text_id in seen_ids: + raise ValueError(f"duplicate text id {text_id}") + if not isinstance(text, str): + raise TypeError("text must be a string") + text_bytes = len(text.encode("utf-8")) + if text_bytes > MAX_TEXT_BYTES: + raise ValueError(f"text {text_id} exceeds {MAX_TEXT_BYTES} UTF-8 bytes") + total_bytes += text_bytes + if total_bytes > MAX_REQUEST_TEXT_BYTES: + raise ValueError(f"request text exceeds {MAX_REQUEST_TEXT_BYTES} UTF-8 bytes") + seen_ids.add(text_id) + texts.append(_InputText(text_id, text)) + return texts, model_id + + +def _split_bio_label(raw_label: str | None) -> tuple[str | None, str | None]: + if raw_label is None or raw_label == "O": + return None, None + if raw_label.startswith(("B-", "I-")) and len(raw_label) > 2: + return raw_label[0], raw_label[2:].upper() + return "B", raw_label.upper() + + +def _merge_overlapping_spans(spans: list[_Span]) -> list[_Span]: + merged: list[_Span] = [] + for span in sorted(spans, key=lambda item: (item.start, -item.end, -item.score, item.label)): + if ( + not merged + or span.start > merged[-1].end + or (span.start == merged[-1].end and span.label != merged[-1].label) + ): + merged.append(span) + continue + previous = merged[-1] + winner = _preferred_span(previous, span) + merged[-1] = _Span( + start=min(previous.start, span.start), + end=max(previous.end, span.end), + label=winner.label, + score=max(previous.score, span.score), + ) + return merged + + +def _preferred_span(left: _Span, right: _Span) -> _Span: + left_key = (left.score, left.end - left.start, left.label) + right_key = (right.score, right.end - right.start, right.label) + return left if left_key >= right_key else right + + +def _utf8_offsets(text: str) -> list[int]: + offsets = [0] + total = 0 + for character in text: + total += len(character.encode("utf-8")) + offsets.append(total) + return offsets + + +def _required_token_id(tokenizer: _Tokenizer, token: str) -> int: + token_id = tokenizer.token_to_id(token) + if token_id is None: + raise ValueError(f"tokenizer is missing required token {token}") + return token_id + + +def _nonempty_string(value: Any, name: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise TypeError(f"{name} must be a non-empty string") + return value + + +def _bounded_string(value: Any, name: str, maximum_bytes: int) -> str: + value = _nonempty_string(value, name) + if len(value.encode("utf-8")) > maximum_bytes: + raise ValueError(f"{name} must not exceed {maximum_bytes} UTF-8 bytes") + return value + + +def _bounded_integer(value: Any, name: str, minimum: int, maximum: int) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{name} must be an integer") + if not minimum <= value <= maximum: + raise ValueError(f"{name} must be between {minimum} and {maximum}") + return value + + +def _optional_bounded_integer(value: Any, name: str, minimum: int, maximum: int) -> int | None: + if value is None: + return None + return _bounded_integer(value, name, minimum, maximum) diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/prefetch.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/prefetch.py new file mode 100644 index 000000000..f75d6b213 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/prefetch.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Prefetch the pinned Rampart model before enabling the worker.""" + +from __future__ import annotations + +import argparse +from collections.abc import Sequence + +from .detector import prefetch_verified_model + + +def main(argv: Sequence[str] | None = None) -> None: + """Download and verify the model in the configured Hugging Face cache.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--cache-dir", + help="Optional Hugging Face cache directory shared with the worker.", + ) + args = parser.parse_args(argv) + model_root = prefetch_verified_model(args.cache_dir) + print(f"Verified Rampart model at {model_root}") + + +if __name__ == "__main__": + main() diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/py.typed b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py new file mode 100644 index 000000000..d1eb58e01 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Manifest entrypoint for the Rampart PII local-model provider.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from nemo_relay_plugin import ConfigDiagnostic, DiagnosticLevel, Json, PluginContext, WorkerPlugin, serve_plugin + +from .detector import RampartDetector, RampartSettings, resolve_verified_model_root + + +class _Admission: + def __init__(self, limit: int) -> None: + self._limit = limit + self._active = 0 + + def acquire(self) -> None: + if self._active >= self._limit: + raise RuntimeError("Rampart provider is at its pending-request limit") + self._active += 1 + + def release(self) -> None: + self._active -= 1 + + +class RampartWorker(WorkerPlugin): + """Expose Rampart inference through the PII component's provider contract.""" + + plugin_id = "nemo_relay.pii_rampart" + + def validate(self, config: Json) -> list[ConfigDiagnostic | dict[str, Any]]: + try: + settings = RampartSettings.from_config(config) + except (TypeError, ValueError) as error: + return [ + ConfigDiagnostic( + level=DiagnosticLevel.ERROR, + code="nemo_relay.pii_rampart.invalid_config", + component=self.plugin_id, + message=str(error), + ) + ] + try: + resolve_verified_model_root(settings) + except Exception: + return [ + ConfigDiagnostic( + level=DiagnosticLevel.ERROR, + code="nemo_relay.pii_rampart.model_unavailable", + component=self.plugin_id, + message=( + "the pinned Rampart model is unavailable or failed integrity " + "verification; prefetch it before enabling the plugin" + ), + ) + ] + return [] + + def register(self, ctx: PluginContext, config: Json) -> None: + settings = RampartSettings.from_config(config) + detector = RampartDetector.load(settings) + admission = _Admission(settings.max_pending_requests) + + async def detect(request: Json) -> Json: + admission.acquire() + work = asyncio.create_task(asyncio.to_thread(detector.detect_request, request)) + release_on_completion = False + try: + return await asyncio.shield(work) + except asyncio.CancelledError: + release_on_completion = True + + def release_after_work(_task: asyncio.Task[Json]) -> None: + try: + _task.exception() + except asyncio.CancelledError: + pass + admission.release() + + work.add_done_callback(release_after_work) + raise + finally: + if not release_on_completion: + admission.release() + + ctx.register_local_model_provider("detector", detect) + + +async def main() -> None: + """Start the Relay-managed worker.""" + await serve_plugin(RampartWorker()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/crates/pii-redaction/providers/rampart/pyproject.toml b/crates/pii-redaction/providers/rampart/pyproject.toml new file mode 100644 index 000000000..62bc82787 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/pyproject.toml @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "nemo-relay-pii-rampart" +version = "0.1.0" +description = "Optional Rampart local-model provider for NeMo Relay PII redaction" +readme = "README.md" +requires-python = ">=3.11" +license = "Apache-2.0" +license-files = ["THIRD_PARTY_NOTICES.md"] +authors = [ + { name = "NVIDIA Corporation & Affiliates" }, +] +dependencies = [ + "huggingface-hub>=0.34,<2", + "nemo-relay-plugin>=0.7,<1.0", + "numpy>=1.26,<3", + "onnxruntime>=1.20,<2", + "tokenizers>=0.21,<1", +] + +[project.scripts] +nemo-relay-pii-rampart-prefetch = "nemo_relay_pii_rampart.prefetch:main" + +[project.optional-dependencies] +test = [ + "pytest>=8", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["nemo_relay_pii_rampart"] + +[tool.setuptools.package-data] +nemo_relay_pii_rampart = ["py.typed"] + +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.format] +quote-style = "double" + +[tool.ruff.lint] +select = ["E", "F", "W", "I"] + +[tool.ruff.lint.isort] +known-first-party = ["nemo_relay_pii_rampart", "nemo_relay_plugin"] + +[tool.ty.analysis] +# The model dependencies live only in the worker's managed environment. +allowed-unresolved-imports = ["huggingface_hub", "numpy", "onnxruntime", "tokenizers"] diff --git a/crates/pii-redaction/providers/rampart/relay-plugin.toml b/crates/pii-redaction/providers/rampart/relay-plugin.toml new file mode 100644 index 000000000..67d382a1e --- /dev/null +++ b/crates/pii-redaction/providers/rampart/relay-plugin.toml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +manifest_version = 1 + +[plugin] +id = "nemo_relay.pii_rampart" +kind = "worker" + +[compat] +relay = ">=0.7,<1.0" +worker_protocol = "grpc-v1" + +[defaults] +enabled = false + +[capabilities] +items = ["plugin_worker", "config_schema"] + +[config_schema] +path = "config.schema.json" + +[source] +manifest_root = "." +artifact = "nemo_relay_pii_rampart/worker.py" + +[integrity] +sha256 = "sha256:9b4bc53676e6c74c12d48212d8ed7e9438b5763001299224724e9bee56a0fda5" + +[load] +runtime = "python" +entrypoint = "nemo_relay_pii_rampart.worker:main" diff --git a/crates/pii-redaction/providers/rampart/tests/test_detector.py b/crates/pii-redaction/providers/rampart/tests/test_detector.py new file mode 100644 index 000000000..9614549d7 --- /dev/null +++ b/crates/pii-redaction/providers/rampart/tests/test_detector.py @@ -0,0 +1,276 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import hashlib +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import numpy as np # ty: ignore[unresolved-import] +import pytest + +import nemo_relay_pii_rampart.detector as detector_module +from nemo_relay_pii_rampart.detector import ( + DEFAULT_MODEL_ID, + RampartDetector, + RampartSettings, + _explicit_model_root, + _merge_overlapping_spans, + _parse_request, + _Span, + _verify_model_files, + prefetch_verified_model, +) + + +class FakeTokenizer: + _tokens = {"[PAD]": 0, "[CLS]": 2, "[SEP]": 3} + + def token_to_id(self, token: str) -> int | None: + return self._tokens.get(token) + + def encode(self, sequence: str, add_special_tokens: bool = True) -> SimpleNamespace: + del add_special_tokens + words = sequence.split() + ids = [] + offsets = [] + cursor = 0 + for index, word in enumerate(words): + start = sequence.index(word, cursor) + end = start + len(word) + ids.append(10 + index) + offsets.append((start, end)) + cursor = end + return SimpleNamespace(ids=ids, type_ids=[0] * len(ids), offsets=offsets) + + +class FakeSession: + def __init__(self, label_ids: list[int], scores: list[float]) -> None: + self._label_ids = label_ids + self._scores = scores + + def get_inputs(self) -> list[SimpleNamespace]: + return [SimpleNamespace(name=name) for name in ("input_ids", "attention_mask", "token_type_ids")] + + def get_outputs(self) -> list[SimpleNamespace]: + return [SimpleNamespace(name="logits")] + + def run(self, output_names: list[str], input_feed: dict[str, np.ndarray]) -> list[np.ndarray]: + assert output_names == ["logits"] + shape = (*input_feed["input_ids"].shape, 5) + logits = np.full(shape, -10.0, dtype=np.float32) + logits[:, :, 0] = 10.0 + for token_index, (label_id, score) in enumerate(zip(self._label_ids, self._scores, strict=True), start=1): + logits[:, token_index, 0] = 0.0 + logits[:, token_index, label_id] = np.log(score / (1.0 - score) * 4.0) + return [logits] + + +class NonFiniteSession(FakeSession): + def run(self, output_names: list[str], input_feed: dict[str, np.ndarray]) -> list[np.ndarray]: + logits = super().run(output_names, input_feed)[0] + logits[0, 0, 0] = np.nan + return [logits] + + +def detector( + label_ids: list[int], + scores: list[float], + *, + max_windows_per_request: int | None = None, +) -> RampartDetector: + config = {} if max_windows_per_request is None else {"max_windows_per_request": max_windows_per_request} + return RampartDetector( + RampartSettings.from_config(config), + FakeTokenizer(), + FakeSession(label_ids, scores), + {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, + ) + + +def test_settings_validate_unknown_and_bounded_fields() -> None: + settings = RampartSettings.from_config({}) + assert settings.model_id == DEFAULT_MODEL_ID + assert settings.local_files_only is True + with pytest.raises(ValueError, match="unknown plugin config"): + RampartSettings.from_config({"surprise": True}) + with pytest.raises(TypeError, match="max_pending_requests"): + RampartSettings.from_config({"max_pending_requests": True}) + with pytest.raises(ValueError, match="model_id"): + RampartSettings.from_config({"model_id": "x" * 1025}) + with pytest.raises(ValueError, match="explicit local directory"): + RampartSettings.from_config({"model_id": "other/model"}) + with pytest.raises(ValueError, match="pinned Rampart revision"): + RampartSettings.from_config({"revision": "main"}) + with pytest.raises(ValueError, match="prefetch"): + RampartSettings.from_config({"local_files_only": False}) + + +def test_model_files_are_verified_before_loading(tmp_path: Path) -> None: + model_file = tmp_path / "model.bin" + model_file.write_bytes(b"trusted model") + expected = {"model.bin": hashlib.sha256(b"trusted model").hexdigest()} + + _verify_model_files(tmp_path, expected) + model_file.write_bytes(b"modified model") + with pytest.raises(ValueError, match="SHA-256 verification"): + _verify_model_files(tmp_path, expected) + + +def test_model_file_verification_rejects_missing_files(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="missing required file"): + _verify_model_files(tmp_path, {"missing.bin": "0" * 64}) + + +def test_prefetch_uses_the_pinned_snapshot_and_verifies_it( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + observed: dict[str, Any] = {} + + def snapshot(model_id: str, **kwargs: Any) -> str: + observed["model_id"] = model_id + observed.update(kwargs) + return str(tmp_path) + + monkeypatch.setattr(detector_module, "snapshot_download", snapshot) + monkeypatch.setattr( + detector_module, "_verify_model_files", lambda model_root: observed.setdefault("root", model_root) + ) + + assert prefetch_verified_model("/tmp/rampart-cache") == tmp_path + assert observed["model_id"] == DEFAULT_MODEL_ID + assert observed["revision"] == detector_module.DEFAULT_MODEL_REVISION + assert observed["local_files_only"] is False + assert observed["cache_dir"] == "/tmp/rampart-cache" + assert observed["root"] == tmp_path + + +def test_local_model_directories_require_explicit_path_syntax( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + model_root = tmp_path / "model" + model_root.mkdir() + assert _explicit_model_root(str(model_root)) == model_root.resolve() + settings = RampartSettings.from_config({"model_id": str(model_root)}) + assert settings.model_id == str(model_root) + + shadow = tmp_path / "nationaldesignstudio" / "rampart" + shadow.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + assert _explicit_model_root(DEFAULT_MODEL_ID) is None + with pytest.raises(ValueError, match="does not exist"): + _explicit_model_root("./missing") + + +def test_request_validation_rejects_duplicate_ids_and_byte_overflow() -> None: + with pytest.raises(ValueError, match="duplicate text id"): + _parse_request( + { + "version": 1, + "texts": [ + {"id": 0, "text": "one"}, + {"id": 0, "text": "two"}, + ], + } + ) + with pytest.raises(ValueError, match="UTF-8 bytes"): + _parse_request({"version": 1, "texts": [{"id": 0, "text": "é" * 9000}]}) + + +def test_detector_returns_utf8_byte_offsets_and_model_labels() -> None: + value = detector([1, 2], [0.99, 0.98]).detect_request({"version": 1, "texts": [{"id": 7, "text": "José Rivera"}]}) + assert value["version"] == 1 + assert len(value["detections"]) == 1 + detection = value["detections"][0] + assert detection["text_id"] == 7 + assert detection["start_utf8"] == 0 + assert detection["end_utf8"] == len("José Rivera".encode()) + assert detection["label"] == "GIVEN_NAME" + assert 0.9 <= detection["score"] <= 1.0 + + city = detector([3, 4], [0.99, 0.98]).detect_request({"version": 1, "texts": [{"id": 0, "text": "New York"}]}) + assert city["detections"][0]["label"] == "CITY" + + +def test_detector_rejects_model_and_profile_mismatch() -> None: + current = detector([], []) + with pytest.raises(ValueError, match="does not match loaded model"): + current.detect_request( + { + "version": 1, + "model_id": "other/model", + "texts": [{"id": 0, "text": ""}], + } + ) + with pytest.raises(ValueError, match="unsupported detector_profile"): + current.detect_request( + { + "version": 1, + "detector_profile": "strict", + "texts": [{"id": 0, "text": ""}], + } + ) + + +def test_local_model_path_keeps_the_logical_model_identity(tmp_path: Path) -> None: + current = RampartDetector( + RampartSettings.from_config({"model_id": str(tmp_path)}), + FakeTokenizer(), + FakeSession([], []), + {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, + ) + result = current.detect_request( + { + "version": 1, + "model_id": DEFAULT_MODEL_ID, + "texts": [{"id": 0, "text": ""}], + } + ) + assert result == {"version": 1, "detections": []} + + +def test_overlapping_window_spans_are_coalesced() -> None: + spans = _merge_overlapping_spans( + [ + _Span(0, 10, "GIVEN_NAME", 0.8), + _Span(5, 12, "SURNAME", 0.9), + _Span(20, 24, "PHONE", 0.7), + _Span(24, 28, "PHONE", 0.8), + _Span(28, 30, "TAX_ID", 0.9), + ] + ) + assert spans == [ + _Span(0, 12, "SURNAME", 0.9), + _Span(20, 28, "PHONE", 0.8), + _Span(28, 30, "TAX_ID", 0.9), + ] + + +def test_request_window_limit_is_enforced_before_inference() -> None: + current = detector([], [], max_windows_per_request=1) + text = " ".join(f"word{index}" for index in range(600)) + with pytest.raises(ValueError, match="max_windows_per_request"): + current.detect_request({"version": 1, "texts": [{"id": 0, "text": text}]}) + + +def test_detector_rejects_invalid_model_outputs() -> None: + with pytest.raises(ValueError, match="contiguous"): + RampartDetector( + RampartSettings(), + FakeTokenizer(), + FakeSession([], []), + {0: "O", 2: "B-GIVEN_NAME"}, + ) + + current = RampartDetector( + RampartSettings(), + FakeTokenizer(), + NonFiniteSession([], []), + {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, + ) + with pytest.raises(ValueError, match="finite"): + current.detect_request({"version": 1, "texts": [{"id": 0, "text": "hello"}]}) diff --git a/crates/pii-redaction/providers/rampart/tests/test_worker.py b/crates/pii-redaction/providers/rampart/tests/test_worker.py new file mode 100644 index 000000000..d703a61ce --- /dev/null +++ b/crates/pii-redaction/providers/rampart/tests/test_worker.py @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import asyncio +import threading +from typing import Any, cast + +import pytest + +import nemo_relay_pii_rampart.worker as worker_module +from nemo_relay_pii_rampart.worker import RampartWorker +from nemo_relay_plugin import ConfigDiagnostic, PluginContext + + +class FakeContext: + def __init__(self) -> None: + self.callback: Any = None + + def register_local_model_provider(self, name: str, callback: Any) -> None: + assert name == "detector" + self.callback = callback + + +class FakeDetector: + def __init__(self, started: threading.Event | None = None, release: threading.Event | None = None) -> None: + self.started = started + self.release = release + + def detect_request(self, request: Any) -> dict[str, Any]: + if self.started is not None: + self.started.set() + if self.release is not None: + self.release.wait(timeout=5) + return {"version": 1, "detections": [], "echo": request} + + +class FailingDetector: + def detect_request(self, request: Any) -> dict[str, Any]: + del request + raise RuntimeError("detector failed") + + +def test_worker_validation_reports_invalid_config(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(worker_module, "resolve_verified_model_root", lambda _settings: None) + worker = RampartWorker() + assert worker.validate({}) == [] + diagnostics = worker.validate({"max_pending_requests": "many"}) + assert len(diagnostics) == 1 + diagnostic = diagnostics[0] + assert isinstance(diagnostic, ConfigDiagnostic) + assert diagnostic.code == "nemo_relay.pii_rampart.invalid_config" + diagnostics = worker.validate({"local_files_only": False}) + assert len(diagnostics) == 1 + diagnostic = diagnostics[0] + assert isinstance(diagnostic, ConfigDiagnostic) + assert diagnostic.code == "nemo_relay.pii_rampart.invalid_config" + + +def test_worker_validation_reports_bounded_model_readiness_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def fail(_settings: Any) -> None: + raise ValueError("/sensitive/cache/path/model_q4.onnx is corrupt") + + monkeypatch.setattr(worker_module, "resolve_verified_model_root", fail) + + diagnostics = RampartWorker().validate({}) + + assert len(diagnostics) == 1 + diagnostic = diagnostics[0] + assert isinstance(diagnostic, ConfigDiagnostic) + assert diagnostic.code == "nemo_relay.pii_rampart.model_unavailable" + assert "prefetch" in diagnostic.message + assert "/sensitive" not in diagnostic.message + + +def test_worker_registers_async_provider(monkeypatch: pytest.MonkeyPatch) -> None: + fake = FakeDetector() + monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: fake) + context = FakeContext() + RampartWorker().register(cast(PluginContext, context), {}) + + request = {"version": 1, "texts": [{"id": 0, "text": "hello"}]} + assert asyncio.run(context.callback(request)) == { + "version": 1, + "detections": [], + "echo": request, + } + + +def test_cancelled_callback_holds_admission_until_native_work_finishes(monkeypatch: pytest.MonkeyPatch) -> None: + started = threading.Event() + release = threading.Event() + fake = FakeDetector(started, release) + monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: fake) + context = FakeContext() + RampartWorker().register(cast(PluginContext, context), {"max_pending_requests": 1}) + + async def exercise() -> None: + first = asyncio.create_task(context.callback({"version": 1, "texts": [{"id": 0, "text": "one"}]})) + assert await asyncio.to_thread(started.wait, 1) + first.cancel() + with pytest.raises(asyncio.CancelledError): + await first + with pytest.raises(RuntimeError, match="pending-request limit"): + await context.callback({"version": 1, "texts": [{"id": 1, "text": "two"}]}) + + release.set() + for _ in range(100): + await asyncio.sleep(0.01) + try: + result = await context.callback({"version": 1, "texts": [{"id": 2, "text": "three"}]}) + except RuntimeError: + continue + assert result["version"] == 1 + return + pytest.fail("provider admission was not released after native work completed") + + asyncio.run(exercise()) + + +def test_detector_failure_releases_admission(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: FailingDetector()) + context = FakeContext() + RampartWorker().register(cast(PluginContext, context), {"max_pending_requests": 1}) + + async def exercise() -> None: + for text_id in range(2): + with pytest.raises(RuntimeError, match="detector failed"): + await context.callback( + { + "version": 1, + "texts": [{"id": text_id, "text": "private"}], + } + ) + + asyncio.run(exercise()) diff --git a/crates/pii-redaction/tests/worker_provider_tests.rs b/crates/pii-redaction/tests/worker_provider_tests.rs new file mode 100644 index 000000000..9d40752cb --- /dev/null +++ b/crates/pii-redaction/tests/worker_provider_tests.rs @@ -0,0 +1,352 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! End-to-end coverage for worker-backed local-model PII redaction. + +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::{Arc, Mutex, OnceLock}; + +use nemo_relay::api::event::Event; +use nemo_relay::api::llm::{LlmCallExecuteParams, LlmRequest, llm_call_execute}; +use nemo_relay::api::runtime::LlmExecutionNextFn; +use nemo_relay::api::scope::{EmitMarkEventParams, event}; +use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber}; +use nemo_relay::codec::openai_chat::OpenAIChatCodec; +use nemo_relay::codec::traits::LlmResponseCodec; +use nemo_relay::plugin::dynamic::{ + DynamicPluginActivationSpec, DynamicPluginKind, PluginHostActivation, +}; +use nemo_relay::plugin::{ + PluginComponentSpec, PluginConfig, clear_plugin_configuration, local_model_provider, +}; +use nemo_relay_pii_redaction::component::{ + PII_REDACTION_PLUGIN_KIND, register_pii_redaction_component, +}; +use serde_json::{Map, json}; +use tempfile::TempDir; + +static WORKER_PII_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + +#[tokio::test(flavor = "multi_thread")] +async fn worker_provider_sanitizes_events_and_is_removed_after_host_clear() { + let _guard = WORKER_PII_TEST_LOCK.lock().await; + let _ = clear_plugin_configuration(); + register_pii_redaction_component().expect("PII component should register"); + let worker_binary = build_fixture_worker(); + let (_manifest_dir, manifest_ref) = write_worker_manifest(&worker_binary); + + let plugin_config = PluginConfig { + version: 1, + components: vec![PluginComponentSpec { + kind: PII_REDACTION_PLUGIN_KIND.into(), + enabled: true, + config: Map::from_iter([ + ("mode".into(), json!("local_model")), + ("codec".into(), json!("openai_chat")), + ("input".into(), json!(true)), + ("output".into(), json!(true)), + ("tool_input".into(), json!(false)), + ("tool_output".into(), json!(false)), + ("mark".into(), json!(true)), + ( + "local".into(), + json!({ + "backend": "fixture_worker/fixture_local_model", + "target_paths": ["/message"], + "target_path_patterns": [ + "/messages/*/content", + "/message" + ] + }), + ), + ]), + }], + policy: Default::default(), + }; + let (activation, report) = PluginHostActivation::activate( + plugin_config, + [DynamicPluginActivationSpec { + plugin_id: "fixture_worker".into(), + kind: DynamicPluginKind::Worker, + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([("provider_only".into(), json!(true))]), + }], + ) + .await + .expect("worker and PII component should activate together"); + assert!(!report.has_errors()); + assert!(local_model_provider("fixture_worker/fixture_local_model").is_ok()); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = Arc::clone(&events); + let subscriber_name = "worker-pii-e2e"; + register_subscriber( + subscriber_name, + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .expect("test subscriber should register"); + + let callback_value = json!({ + "message": "keep PRIVATE hidden", + "unselected": "PRIVATE remains outside the configured path" + }); + event( + EmitMarkEventParams::builder() + .name("worker-pii") + .data(callback_value.clone()) + .build(), + ) + .expect("mark should emit"); + flush_subscribers().expect("sanitized event should flush"); + + { + let captured = events.lock().unwrap(); + assert_eq!(captured.len(), 1); + assert_eq!( + captured[0].data(), + Some(&json!({ + "message": "keep [REDACTED] hidden", + "unselected": "PRIVATE remains outside the configured path" + })) + ); + } + assert_eq!( + callback_value["message"], "keep PRIVATE hidden", + "sanitization must not mutate caller-owned JSON" + ); + + let callback_request = Arc::new(Mutex::new(None)); + let observed_request = Arc::clone(&callback_request); + let response = json!({ + "id": "chatcmpl-PRIVATE", + "model": "model-PRIVATE", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "answer for PRIVATE" + }, + "finish_reason": "stop" + }] + }); + let callback_response = response.clone(); + let callback: LlmExecutionNextFn = Arc::new(move |request| { + *observed_request.lock().unwrap() = Some(request.clone()); + let response = callback_response.clone(); + Box::pin(async move { Ok(response) }) + }); + let request = LlmRequest { + headers: Map::new(), + content: json!({ + "model": "model-PRIVATE", + "messages": [ + {"role": "system", "content": "policy"}, + {"role": "user", "content": "question from PRIVATE"} + ], + "vendor_trace": "trace-PRIVATE" + }), + }; + let response_codec: Arc = Arc::new(OpenAIChatCodec); + + let returned = llm_call_execute( + LlmCallExecuteParams::builder() + .name("openai") + .request(request.clone()) + .func(callback) + .response_codec(response_codec) + .build(), + ) + .await + .expect("LLM callback should complete"); + flush_subscribers().expect("LLM events should flush"); + + assert_eq!( + returned, response, + "sanitize guardrails must not rewrite callback values" + ); + assert_eq!( + callback_request.lock().unwrap().as_ref(), + Some(&request), + "sanitize guardrails must not rewrite provider requests" + ); + let captured = events.lock().unwrap(); + assert_eq!(captured.len(), 3); + assert_eq!( + captured[1].input().unwrap()["content"]["messages"][1]["content"], + "question from [REDACTED]" + ); + assert_eq!( + captured[1].input().unwrap()["content"]["model"], + "model-PRIVATE", + "model identifiers are outside the selected content paths" + ); + assert_eq!( + captured[1].input().unwrap()["content"]["vendor_trace"], + "trace-PRIVATE", + "provider metadata is outside the selected content paths" + ); + assert_eq!( + captured[2].output().unwrap()["choices"][0]["message"]["content"], + "answer for [REDACTED]" + ); + assert_eq!(captured[2].output().unwrap()["id"], "chatcmpl-PRIVATE"); + assert_eq!(captured[2].output().unwrap()["model"], "model-PRIVATE"); + drop(captured); + + deregister_subscriber(subscriber_name).expect("test subscriber should deregister"); + activation.clear().expect("plugin host should clear"); + assert!( + local_model_provider("fixture_worker/fixture_local_model").is_err(), + "worker provider should not outlive its host activation" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn worker_exit_during_sanitization_fails_closed_and_removes_provider() { + let _guard = WORKER_PII_TEST_LOCK.lock().await; + let _ = clear_plugin_configuration(); + register_pii_redaction_component().expect("PII component should register"); + let worker_binary = build_fixture_worker(); + let (_manifest_dir, manifest_ref) = write_worker_manifest(&worker_binary); + let plugin_config = PluginConfig { + version: 1, + components: vec![PluginComponentSpec { + kind: PII_REDACTION_PLUGIN_KIND.into(), + enabled: true, + config: Map::from_iter([ + ("mode".into(), json!("local_model")), + ("input".into(), json!(false)), + ("output".into(), json!(false)), + ("tool_input".into(), json!(false)), + ("tool_output".into(), json!(false)), + ("mark".into(), json!(true)), + ( + "local".into(), + json!({ + "backend": "fixture_worker/fixture_local_model", + "target_paths": ["/message"] + }), + ), + ]), + }], + policy: Default::default(), + }; + let (activation, report) = PluginHostActivation::activate( + plugin_config, + [DynamicPluginActivationSpec { + plugin_id: "fixture_worker".into(), + kind: DynamicPluginKind::Worker, + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([ + ("provider_only".into(), json!(true)), + ("exit_in_local_model".into(), json!(true)), + ]), + }], + ) + .await + .expect("worker and PII component should activate together"); + assert!(!report.has_errors()); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = Arc::clone(&events); + let subscriber_name = "worker-pii-exit"; + register_subscriber( + subscriber_name, + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .expect("test subscriber should register"); + + event( + EmitMarkEventParams::builder() + .name("worker-pii-exit") + .data(json!({ + "message": "PRIVATE", + "unselected": "PRIVATE" + })) + .build(), + ) + .expect("worker failure must not block event emission"); + flush_subscribers().expect("fail-closed event should flush"); + assert_eq!( + events.lock().unwrap()[0].data(), + Some(&json!({ + "message": "[REDACTED]", + "unselected": "PRIVATE" + })) + ); + + deregister_subscriber(subscriber_name).expect("test subscriber should deregister"); + let error = activation + .clear() + .expect_err("stopped worker shutdown should be reported") + .to_string(); + assert!(error.contains("shutdown"), "{error}"); + assert!( + local_model_provider("fixture_worker/fixture_local_model").is_err(), + "failed worker provider should not survive host teardown" + ); +} + +fn build_fixture_worker() -> PathBuf { + static FIXTURE_BINARY: OnceLock = OnceLock::new(); + FIXTURE_BINARY + .get_or_init(|| { + let manifest = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../core/tests/fixtures/worker_plugin/Cargo.toml"); + let target_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../target/worker-plugin-fixture/target"); + let status = Command::new("cargo") + .arg("build") + .arg("--quiet") + .arg("--locked") + .arg("--manifest-path") + .arg(&manifest) + .arg("--target-dir") + .arg(&target_dir) + .status() + .expect("fixture worker build should start"); + assert!(status.success(), "fixture worker build should succeed"); + let binary = target_dir.join("debug").join(format!( + "nemo-relay-worker-plugin-fixture{}", + std::env::consts::EXE_SUFFIX + )); + assert!(binary.exists(), "fixture worker binary should exist"); + binary + }) + .clone() +} + +fn write_worker_manifest(binary: &Path) -> (TempDir, PathBuf) { + let temp = TempDir::new().expect("manifest directory should be created"); + let manifest = temp.path().join("relay-plugin.toml"); + let contents = format!( + r#" +manifest_version = 1 + +[plugin] +id = "fixture_worker" +kind = "worker" + +[compat] +relay = "={version}" +worker_protocol = "grpc-v1" + +[defaults] +enabled = false + +[capabilities] +items = ["plugin_worker"] + +[load] +runtime = "rust" +entrypoint = {entrypoint:?} +"#, + version = env!("CARGO_PKG_VERSION"), + entrypoint = binary.to_string_lossy(), + ); + std::fs::write(&manifest, contents).expect("worker manifest should be written"); + (temp, manifest) +} From fe72547824891704a1cccabbb84f8c8c371abb0d Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sun, 26 Jul 2026 09:01:38 -0700 Subject: [PATCH 04/83] feat(plugin): add contract-aware inference providers Signed-off-by: Alex Fournier --- crates/cli/src/server/mod.rs | 7 +- crates/core/src/lib.rs | 4 +- crates/core/src/plugin.rs | 124 ++++++-- crates/core/src/plugin/dynamic/host.rs | 24 +- crates/core/src/plugin/dynamic/worker.rs | 105 +++++-- crates/core/src/plugin/inference.rs | 219 ++++++++++++++ crates/core/src/plugin/local_model.rs | 108 ------- .../tests/fixtures/worker_plugin/src/main.rs | 16 +- .../tests/integration/worker_plugin_tests.rs | 169 +++++++---- .../core/tests/unit/dynamic_worker_tests.rs | 42 +++ .../tests/unit/inference_provider_tests.rs | 117 ++++++++ crates/core/tests/unit/local_model_tests.rs | 70 ----- crates/core/tests/unit/plugin_tests.rs | 98 ++++++ crates/pii-redaction/README.md | 12 +- .../nemo_relay_pii_rampart/__init__.py | 2 +- .../rampart/nemo_relay_pii_rampart/worker.py | 10 +- .../providers/rampart/pyproject.toml | 2 +- .../providers/rampart/tests/test_worker.py | 3 +- crates/pii-redaction/src/component.rs | 2 + crates/pii-redaction/src/local.rs | 32 +- .../tests/unit/component_tests.rs | 151 +++++++--- .../pii-redaction/tests/unit/local_tests.rs | 281 +++++++----------- .../tests/worker_provider_tests.rs | 31 +- crates/worker-proto/README.md | 6 +- .../nemo/relay/worker/v1/plugin_worker.proto | 4 +- crates/worker-proto/tests/proto_tests.rs | 11 +- crates/worker/README.md | 26 +- crates/worker/src/lib.rs | 44 ++- crates/worker/tests/worker_sdk_tests.rs | 15 +- .../grpc-worker/grpc-worker-protocol.mdx | 12 +- .../grpc-worker/python/about.mdx | 15 +- .../grpc-worker/rust/about.mdx | 25 +- .../pii-redaction/configuration.mdx | 6 +- .../worker.py | 6 +- .../relay-plugin.toml | 2 +- justfile | 3 +- python/plugin/README.md | 18 +- .../plugin/src/nemo_relay_plugin/__init__.py | 6 +- python/plugin/src/nemo_relay_plugin/_api.py | 37 ++- .../plugin/test_public_api_docstrings.py | 2 +- python/tests/plugin/test_worker_sdk.py | 55 ++-- 41 files changed, 1246 insertions(+), 676 deletions(-) create mode 100644 crates/core/src/plugin/inference.rs delete mode 100644 crates/core/src/plugin/local_model.rs create mode 100644 crates/core/tests/unit/inference_provider_tests.rs delete mode 100644 crates/core/tests/unit/local_model_tests.rs diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index a263f0a54..2bfc568f9 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -25,6 +25,7 @@ use nemo_relay::plugin::dynamic::{ }; use nemo_relay::plugin::{ PluginComponentSpec, PluginConfig, clear_plugin_configuration, initialize_plugins_exact, + initialize_plugins_exact_with_inference_providers, }; use nemo_relay_adaptive::plugin_component::register_adaptive_component; use nemo_relay_pii_redaction::component::register_pii_redaction_component; @@ -1116,7 +1117,11 @@ impl PluginActivation { CliError::Config(format!("worker plugin load failed: {error}")) })?) }; - initialize_plugins_exact(plugin_config) + let inference_providers = worker + .as_ref() + .map(WorkerPluginActivation::inference_providers) + .unwrap_or_default(); + initialize_plugins_exact_with_inference_providers(plugin_config, inference_providers) .await .map_err(|error| CliError::Config(format!("plugin activation failed: {error}")))?; Ok(Self { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 92e828d52..6505e1e5f 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -70,8 +70,8 @@ pub mod shared_runtime; pub mod stream; #[cfg(test)] -#[path = "../tests/unit/local_model_tests.rs"] -mod local_model_tests; +#[path = "../tests/unit/inference_provider_tests.rs"] +mod inference_provider_tests; #[cfg(test)] #[path = "../tests/unit/types_tests.rs"] mod types_tests; diff --git a/crates/core/src/plugin.rs b/crates/core/src/plugin.rs index ae8a7c0c6..0a8305d8a 100644 --- a/crates/core/src/plugin.rs +++ b/crates/core/src/plugin.rs @@ -48,13 +48,11 @@ pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel}; pub mod dynamic; pub use dynamic::*; -mod local_model; -#[cfg(feature = "worker-grpc")] -pub(crate) use local_model::deregister_local_model_provider_checked; +mod inference; #[doc(hidden)] -pub use local_model::{ - LocalModelProviderFn, deregister_local_model_provider, local_model_provider, - register_local_model_provider_tracked, +pub use inference::{ + InferenceProvider, InferenceProviderDescriptor, InferenceProviderFn, + InferenceProviderRegistration, InferenceProviderRegistry, }; type PluginMap = HashMap; @@ -350,6 +348,7 @@ impl PluginRegistration { pub struct PluginRegistrationContext { registrations: Vec, namespace: Option, + inference_providers: InferenceProviderRegistry, } impl PluginRegistrationContext { @@ -363,9 +362,33 @@ impl PluginRegistrationContext { Self { registrations: vec![], namespace: Some(namespace.into()), + inference_providers: InferenceProviderRegistry::default(), } } + /// Creates a registration context backed by host-owned inference providers. + #[doc(hidden)] + pub fn with_inference_providers( + namespace: Option, + inference_providers: InferenceProviderRegistry, + ) -> Self { + Self { + registrations: Vec::new(), + namespace, + inference_providers, + } + } + + /// Resolves an inference provider implementing the required contract. + #[doc(hidden)] + pub fn inference_provider( + &self, + name: &str, + expected_contract: &str, + ) -> Result { + self.inference_providers.resolve(name, expected_contract) + } + /// Returns the runtime-qualified name for a plugin-local registration. /// /// Plugin handlers should pass stable component-local names such as @@ -1317,12 +1340,23 @@ pub fn plugin_config_schema() -> Json { /// is removed before the new configuration is activated. #[doc(hidden)] pub async fn initialize_plugins_exact(config: PluginConfig) -> Result { + initialize_plugins_exact_with_inference_providers(config, InferenceProviderRegistry::default()) + .await +} + +/// Configures plugin components with host-owned inference providers. +#[doc(hidden)] +pub async fn initialize_plugins_exact_with_inference_providers( + config: PluginConfig, + inference_providers: InferenceProviderRegistry, +) -> Result { run_owned_plugin_mutation("plugin initialization", move || async move { let lease = LegacyPluginMutationLease::acquire()?; let rollback_failures = Arc::new(Mutex::new(Vec::new())); let initialization = tokio::spawn(initialize_plugins_exact_inner( config, Some(Arc::clone(&rollback_failures)), + inference_providers, )) .await .map_err(|error| { @@ -1439,14 +1473,16 @@ pub(crate) async fn initialize_plugins_exact_for_host( config: PluginConfig, owner_id: u64, rollback_failures: Arc>>, + inference_providers: InferenceProviderRegistry, ) -> Result { verify_plugin_host_owner(owner_id)?; - initialize_plugins_exact_inner(config, Some(rollback_failures)).await + initialize_plugins_exact_inner(config, Some(rollback_failures), inference_providers).await } async fn initialize_plugins_exact_inner( config: PluginConfig, rollback_failures: Option>>>, + inference_providers: InferenceProviderRegistry, ) -> Result { let enabled_component_count = config .components @@ -1483,11 +1519,17 @@ async fn initialize_plugins_exact_inner( match initialize_plugin_components_catching_panics( config.clone(), rollback_failures.clone(), + inference_providers.clone(), ) .await { Ok(registrations) => { - store_active_plugin_configuration(config, report.clone(), registrations)?; + store_active_plugin_configuration( + config, + report.clone(), + registrations, + inference_providers, + )?; log::info!( target: "nemo_relay.plugin", event = "plugin_configuration_replaced", @@ -1499,6 +1541,7 @@ async fn initialize_plugins_exact_inner( Err(err) => match initialize_plugin_components_catching_panics( previous_state.config.clone(), rollback_failures.clone(), + previous_state.inference_providers.clone(), ) .await { @@ -1508,6 +1551,7 @@ async fn initialize_plugins_exact_inner( previous_state.config, previous_report, registrations, + previous_state.inference_providers, )?; log::warn!( target: "nemo_relay.plugin", @@ -1531,9 +1575,18 @@ async fn initialize_plugins_exact_inner( }, } } else { - let registrations = - initialize_plugin_components_catching_panics(config.clone(), rollback_failures).await?; - store_active_plugin_configuration(config, report.clone(), registrations)?; + let registrations = initialize_plugin_components_catching_panics( + config.clone(), + rollback_failures, + inference_providers.clone(), + ) + .await?; + store_active_plugin_configuration( + config, + report.clone(), + registrations, + inference_providers, + )?; log::info!( target: "nemo_relay.plugin", event = "plugin_configuration_activated", @@ -1547,14 +1600,17 @@ async fn initialize_plugins_exact_inner( async fn initialize_plugin_components_catching_panics( config: PluginConfig, rollback_failures: Option>>>, + inference_providers: InferenceProviderRegistry, ) -> Result> { - tokio::spawn(async move { initialize_plugin_components(&config, rollback_failures).await }) - .await - .map_err(|error| { - PluginError::Internal(format!( - "plugin component initialization task failed: {error}" - )) - })? + tokio::spawn(async move { + initialize_plugin_components(&config, rollback_failures, inference_providers).await + }) + .await + .map_err(|error| { + PluginError::Internal(format!( + "plugin component initialization task failed: {error}" + )) + })? } /// Validates and activates `config` layered on top of the discovered @@ -1567,6 +1623,16 @@ pub async fn initialize_plugins(config: PluginConfig) -> Result { initialize_plugins_exact(config).await } +/// Resolves discovered configuration and activates it with host inference providers. +#[doc(hidden)] +pub async fn initialize_plugins_with_inference_providers( + config: PluginConfig, + inference_providers: InferenceProviderRegistry, +) -> Result { + let config = resolve_plugin_config(config)?; + initialize_plugins_exact_with_inference_providers(config, inference_providers).await +} + /// Layers `config` over the default discovered `plugins.toml` files. /// /// This is crate-visible so owned dynamic-plugin activation can use the same @@ -1963,11 +2029,13 @@ struct ActivePluginConfiguration { config: PluginConfig, report: ConfigReport, registrations: Vec, + inference_providers: InferenceProviderRegistry, } async fn initialize_plugin_components( config: &PluginConfig, rollback_failures: Option>>>, + inference_providers: InferenceProviderRegistry, ) -> Result> { ensure_builtin_plugins_registered()?; let totals = plugin_component_totals(config); @@ -1996,8 +2064,11 @@ async fn initialize_plugin_components( totals.get(component.kind.as_str()).copied().unwrap_or(1), ); - let mut pending = - PendingPluginRegistrationContext::new(namespace, rollback_failures.clone()); + let mut pending = PendingPluginRegistrationContext::new( + namespace, + rollback_failures.clone(), + inference_providers.clone(), + ); plugin .register(&component.config, &mut pending.context) .await?; @@ -2042,9 +2113,16 @@ struct PendingPluginRegistrationContext { } impl PendingPluginRegistrationContext { - fn new(namespace: String, rollback_failures: Option>>>) -> Self { + fn new( + namespace: String, + rollback_failures: Option>>>, + inference_providers: InferenceProviderRegistry, + ) -> Self { Self { - context: PluginRegistrationContext::with_namespace(namespace), + context: PluginRegistrationContext::with_inference_providers( + Some(namespace), + inference_providers, + ), rollback_failures, } } @@ -2079,6 +2157,7 @@ fn store_active_plugin_configuration( config: PluginConfig, report: ConfigReport, registrations: Vec, + inference_providers: InferenceProviderRegistry, ) -> Result<()> { let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| { PluginError::Internal(format!("active plugin configuration lock poisoned: {err}")) @@ -2087,6 +2166,7 @@ fn store_active_plugin_configuration( config, report, registrations, + inference_providers, }); Ok(()) } diff --git a/crates/core/src/plugin/dynamic/host.rs b/crates/core/src/plugin/dynamic/host.rs index 990becc38..fe55a803d 100644 --- a/crates/core/src/plugin/dynamic/host.rs +++ b/crates/core/src/plugin/dynamic/host.rs @@ -16,8 +16,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as Json}; use crate::plugin::{ - ConfigReport, PluginComponentSpec, PluginConfig, PluginHostLease, Result, - acquire_plugin_host_lease, clear_plugin_configuration_for_host, + ConfigReport, InferenceProviderRegistry, PluginComponentSpec, PluginConfig, PluginHostLease, + Result, acquire_plugin_host_lease, clear_plugin_configuration_for_host, ensure_builtin_plugins_registered, initialize_plugins_exact_for_host, resolve_plugin_config, run_owned_plugin_mutation, }; @@ -186,10 +186,18 @@ impl PluginHostActivation { ); let rollback_failures = Arc::new(Mutex::new(Vec::new())); let owner_id = claim.owner_id(); + #[cfg(feature = "worker-grpc")] + let inference_providers = worker + .as_ref() + .map(WorkerPluginActivation::inference_providers) + .unwrap_or_else(InferenceProviderRegistry::default); + #[cfg(not(feature = "worker-grpc"))] + let inference_providers = InferenceProviderRegistry::default(); let initialization = tokio::spawn(initialize_plugins_exact_for_host( config, owner_id, Arc::clone(&rollback_failures), + inference_providers, )) .await .map_err(|error| { @@ -262,6 +270,16 @@ impl PluginHostActivation { self.active } + /// Returns the inference providers owned by this activation. + #[doc(hidden)] + pub fn inference_providers(&self) -> InferenceProviderRegistry { + #[cfg(feature = "worker-grpc")] + if let Some(worker) = &self.worker { + return worker.inference_providers(); + } + InferenceProviderRegistry::default() + } + /// Clear registered callbacks before unloading libraries and workers. pub fn clear(mut self) -> Result<()> { self.clear_inner() @@ -300,7 +318,7 @@ impl PluginHostActivation { #[cfg(feature = "worker-grpc")] if let Some(worker) = &mut self.worker { runtime_outcome.merge(worker.deregister_plugin_kinds_checked()); - runtime_outcome.merge(worker.deregister_local_model_providers_checked()); + runtime_outcome.merge(worker.deregister_inference_providers_checked()); } // A worker cannot be stopped while its registry adapter might still be diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 04c1f17db..5bb5e2cbc 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -72,10 +72,9 @@ use crate::codec::request::{ANNOTATED_LLM_REQUEST_SCHEMA, AnnotatedLlmRequest}; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::error::{FlowError, Result as FlowResult}; use crate::plugin::{ - ConfigDiagnostic, DiagnosticLevel, Plugin, PluginDeregistrationOutcome, PluginError, - PluginRegistrationContext, deregister_local_model_provider_checked, - deregister_plugin_registration_checked, register_local_model_provider_tracked, - register_plugin_tracked, + ConfigDiagnostic, DiagnosticLevel, InferenceProviderDescriptor, InferenceProviderRegistration, + InferenceProviderRegistry, Plugin, PluginDeregistrationOutcome, PluginError, + PluginRegistrationContext, deregister_plugin_registration_checked, register_plugin_tracked, }; use super::{ @@ -131,7 +130,8 @@ pub struct WorkerPluginLoadSpec { pub struct WorkerPluginActivation { plugins: Vec>, plugin_registrations: Vec<(String, u64)>, - local_model_registrations: Vec<(String, u64)>, + inference_providers: InferenceProviderRegistry, + inference_provider_registrations: Vec, } impl WorkerPluginActivation { @@ -143,14 +143,20 @@ impl WorkerPluginActivation { /// Consumes the activation; deregistration runs from `Drop`. pub fn clear(self) {} + /// Returns the host-owned inference providers installed by this activation. + #[doc(hidden)] + pub fn inference_providers(&self) -> InferenceProviderRegistry { + self.inference_providers.clone() + } + pub(crate) fn deregister_plugin_kinds_checked(&mut self) -> DynamicPluginTeardownOutcome { deregister_tracked_registrations_checked(&mut self.plugin_registrations, "worker") } - pub(crate) fn deregister_local_model_providers_checked( + pub(crate) fn deregister_inference_providers_checked( &mut self, ) -> DynamicPluginTeardownOutcome { - deregister_local_model_providers_checked(&mut self.local_model_registrations) + deregister_inference_providers_checked(&mut self.inference_provider_registrations) } pub(crate) fn shutdown_plugins_checked(&self) -> DynamicPluginTeardownOutcome { @@ -164,7 +170,7 @@ impl WorkerPluginActivation { impl Drop for WorkerPluginActivation { fn drop(&mut self) { - let _ = deregister_local_model_providers_checked(&mut self.local_model_registrations); + let _ = deregister_inference_providers_checked(&mut self.inference_provider_registrations); for (plugin_kind, registration_id) in self.plugin_registrations.iter().rev() { let _ = deregister_plugin_registration_checked(plugin_kind, *registration_id); } @@ -176,24 +182,38 @@ impl Drop for WorkerPluginActivation { /// The returned activation must be kept alive until after active plugin /// configuration has been cleared. pub fn load_worker_plugins(specs: I) -> crate::plugin::Result +where + I: IntoIterator, +{ + load_worker_plugins_with_inference_providers(specs, InferenceProviderRegistry::default()) +} + +/// Loads worker plugins into an existing host inference-provider registry. +#[doc(hidden)] +pub fn load_worker_plugins_with_inference_providers( + specs: I, + inference_providers: InferenceProviderRegistry, +) -> crate::plugin::Result where I: IntoIterator, { let mut activation = WorkerPluginActivation { plugins: Vec::new(), plugin_registrations: Vec::new(), - local_model_registrations: Vec::new(), + inference_providers: inference_providers.clone(), + inference_provider_registrations: Vec::new(), }; for spec in specs { let instance = load_one_worker_plugin(&spec)?; - let local_model_registrations = instance.install_local_model_providers()?; + let inference_provider_registrations = + instance.install_inference_providers(&inference_providers)?; let plugin_kind = instance.plugin_kind.clone(); // Transfer ownership before the next fallible registration so Drop can // unwind providers and the worker process on partial activation. activation.plugins.push(instance.clone()); activation - .local_model_registrations - .extend(local_model_registrations); + .inference_provider_registrations + .extend(inference_provider_registrations); let registration_id = register_plugin_tracked(Arc::new(WorkerPluginAdapter { plugin_kind: plugin_kind.clone(), allows_multiple_components: instance.allows_multiple_components, @@ -1060,7 +1080,10 @@ fn clear_host_python_environment(command: &mut Command) { } impl WorkerPluginInstance { - fn install_local_model_providers(&self) -> crate::plugin::Result> { + fn install_inference_providers( + &self, + registry: &InferenceProviderRegistry, + ) -> crate::plugin::Result> { let mut registrations = Vec::new(); for registration in &self.registrations { let surface = RegistrationSurface::try_from(registration.surface).map_err(|_| { @@ -1069,21 +1092,23 @@ impl WorkerPluginInstance { self.plugin_kind, registration.surface )) })?; - if surface != RegistrationSurface::LocalModelProvider { + if surface != RegistrationSurface::InferenceProvider { continue; } let callback_name = registration.local_name.clone(); let provider_name = format!("{}/{}", self.plugin_kind, callback_name); let callback = self.clone_for_callback(); - match register_local_model_provider_tracked( - &provider_name, + let descriptor = + InferenceProviderDescriptor::new(provider_name, registration.contract.clone())?; + match registry.register( + descriptor, Arc::new(move |request, timeout| { - callback.invoke_local_model_provider(&callback_name, request, timeout) + callback.invoke_inference_provider(&callback_name, request, timeout) }), ) { - Ok(registration_id) => registrations.push((provider_name, registration_id)), + Ok(registration) => registrations.push(registration), Err(error) => { - let _ = deregister_local_model_providers_checked(&mut registrations); + let _ = deregister_inference_providers_checked(&mut registrations); return Err(error); } } @@ -1130,9 +1155,9 @@ impl WorkerPluginInstance { | RegistrationSurface::LlmStreamExecutionIntercept => { self.install_llm_registration(ctx, registration, surface)? } - RegistrationSurface::LocalModelProvider => { - // Providers are installed before static component - // initialization so components can resolve them. + RegistrationSurface::InferenceProvider => { + // Providers are installed during host bootstrap so components + // can resolve their contracts before runtime callbacks register. } RegistrationSurface::Unspecified => { return Err(PluginError::RegistrationFailed(format!( @@ -1421,7 +1446,7 @@ struct WorkerPluginCallback { } impl WorkerPluginCallback { - fn invoke_local_model_provider( + fn invoke_inference_provider( &self, registration_name: &str, value: Json, @@ -1429,7 +1454,7 @@ impl WorkerPluginCallback { ) -> crate::plugin::Result { let request = self.base_request( registration_name, - RegistrationSurface::LocalModelProvider, + RegistrationSurface::InferenceProvider, None, Some(invoke_request_payload::Payload::Provider( json_envelope_infallible(JSON_SCHEMA, &value), @@ -1441,12 +1466,12 @@ impl WorkerPluginCallback { ) .map_err(|error| { PluginError::RegistrationFailed(format!( - "local-model provider '{registration_name}' invocation failed: {error}" + "inference provider '{registration_name}' invocation failed: {error}" )) })?; json_from_invoke_response(response).map_err(|error| { PluginError::RegistrationFailed(format!( - "local-model provider '{registration_name}' returned an invalid response: {error}" + "inference provider '{registration_name}' returned an invalid response: {error}" )) }) } @@ -1463,25 +1488,26 @@ impl WorkerPluginCallback { } } -fn deregister_local_model_providers_checked( - registrations: &mut Vec<(String, u64)>, +fn deregister_inference_providers_checked( + registrations: &mut Vec, ) -> DynamicPluginTeardownOutcome { let mut outcome = DynamicPluginTeardownOutcome::success(); - for (name, registration_id) in std::mem::take(registrations).into_iter().rev() { - match deregister_local_model_provider_checked(&name, registration_id) { + for mut registration in std::mem::take(registrations).into_iter().rev() { + let name = registration.name().to_string(); + match registration.deregister_checked() { Ok(PluginDeregistrationOutcome::Removed) => {} Ok(PluginDeregistrationOutcome::Missing) => outcome.record_error( - format!("local-model provider '{name}' was not registered during teardown"), + format!("inference provider '{name}' was not registered during teardown"), true, ), Ok(PluginDeregistrationOutcome::Replaced) => outcome.record_error( format!( - "local-model provider '{name}' was replaced during teardown and was left registered" + "inference provider '{name}' was replaced during teardown and was left registered" ), true, ), Err(error) => outcome.record_error( - format!("failed to deregister local-model provider '{name}': {error}"), + format!("failed to deregister inference provider '{name}': {error}"), false, ), } @@ -3056,6 +3082,19 @@ fn validate_registration_plan( "worker plugin '{plugin_id}' returned unspecified registration surface" ))); } + let contract = registration.contract.trim(); + if surface == RegistrationSurface::InferenceProvider && contract.is_empty() { + return Err(PluginError::RegistrationFailed(format!( + "worker plugin '{plugin_id}' returned inference provider '{}' without a contract", + registration.local_name + ))); + } + if surface != RegistrationSurface::InferenceProvider && !contract.is_empty() { + return Err(PluginError::RegistrationFailed(format!( + "worker plugin '{plugin_id}' returned a contract for non-provider registration '{}'", + registration.local_name + ))); + } } Ok(()) } diff --git a/crates/core/src/plugin/inference.rs b/crates/core/src/plugin/inference.rs new file mode 100644 index 000000000..fa0b6a43b --- /dev/null +++ b/crates/core/src/plugin/inference.rs @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Host-owned inference-provider services used by plugin components. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; + +use serde_json::Value as Json; + +use super::{PluginDeregistrationOutcome, PluginError, Result}; + +/// Versioned JSON request-response callback implemented by an inference provider. +#[doc(hidden)] +pub type InferenceProviderFn = Arc Result + Send + Sync + 'static>; + +/// Stable identity and request-response contract for one inference provider. +#[doc(hidden)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InferenceProviderDescriptor { + name: String, + contract: String, +} + +impl InferenceProviderDescriptor { + /// Creates a provider descriptor after validating its stable identifiers. + pub fn new(name: impl Into, contract: impl Into) -> Result { + let name = normalized_identifier(name.into(), "inference provider name")?; + let contract = normalized_identifier(contract.into(), "inference provider contract")?; + Ok(Self { name, contract }) + } + + /// Returns the host-qualified provider name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the versioned request-response contract identifier. + pub fn contract(&self) -> &str { + &self.contract + } +} + +/// Resolved inference provider whose contract has already been checked. +#[doc(hidden)] +#[derive(Clone)] +pub struct InferenceProvider { + descriptor: InferenceProviderDescriptor, + callback: InferenceProviderFn, +} + +impl InferenceProvider { + /// Returns the provider descriptor. + pub fn descriptor(&self) -> &InferenceProviderDescriptor { + &self.descriptor + } + + /// Invokes the provider with the component-owned request and deadline. + pub fn invoke(&self, request: Json, timeout: Duration) -> Result { + (self.callback)(request, timeout) + } +} + +struct RegisteredInferenceProvider { + registration_id: u64, + descriptor: InferenceProviderDescriptor, + callback: InferenceProviderFn, +} + +struct InferenceProviderRegistryInner { + providers: RwLock>, + next_registration_id: AtomicU64, +} + +/// Host-scoped registry for versioned inference providers. +#[doc(hidden)] +#[derive(Clone)] +pub struct InferenceProviderRegistry { + inner: Arc, +} + +impl Default for InferenceProviderRegistry { + fn default() -> Self { + Self { + inner: Arc::new(InferenceProviderRegistryInner { + providers: RwLock::new(HashMap::new()), + next_registration_id: AtomicU64::new(1), + }), + } + } +} + +impl InferenceProviderRegistry { + /// Registers a provider and returns an ownership handle. + pub fn register( + &self, + descriptor: InferenceProviderDescriptor, + callback: InferenceProviderFn, + ) -> Result { + let mut providers = self.inner.providers.write().map_err(|error| { + PluginError::Internal(format!( + "inference provider registry lock poisoned: {error}" + )) + })?; + if providers.contains_key(descriptor.name()) { + return Err(PluginError::RegistrationFailed(format!( + "inference provider '{}' is already registered", + descriptor.name() + ))); + } + let registration_id = self + .inner + .next_registration_id + .fetch_add(1, Ordering::Relaxed); + let name = descriptor.name().to_string(); + providers.insert( + name.clone(), + RegisteredInferenceProvider { + registration_id, + descriptor, + callback, + }, + ); + Ok(InferenceProviderRegistration { + registry: self.clone(), + name, + registration_id: Some(registration_id), + }) + } + + /// Resolves a provider only when its declared contract exactly matches. + pub fn resolve(&self, name: &str, expected_contract: &str) -> Result { + let name = normalized_identifier(name.to_string(), "inference provider name")?; + let expected_contract = + normalized_identifier(expected_contract.to_string(), "inference provider contract")?; + let providers = self.inner.providers.read().map_err(|error| { + PluginError::Internal(format!( + "inference provider registry lock poisoned: {error}" + )) + })?; + let provider = providers.get(&name).ok_or_else(|| { + PluginError::NotFound(format!("inference provider '{name}' is not registered")) + })?; + if provider.descriptor.contract() != expected_contract { + return Err(PluginError::RegistrationFailed(format!( + "inference provider '{name}' implements contract '{}' but '{}' is required", + provider.descriptor.contract(), + expected_contract + ))); + } + Ok(InferenceProvider { + descriptor: provider.descriptor.clone(), + callback: Arc::clone(&provider.callback), + }) + } + + fn deregister(&self, name: &str, registration_id: u64) -> Result { + let mut providers = self.inner.providers.write().map_err(|error| { + PluginError::Internal(format!( + "inference provider registry lock poisoned: {error}" + )) + })?; + match providers.get(name) { + Some(provider) if provider.registration_id == registration_id => { + providers.remove(name); + Ok(PluginDeregistrationOutcome::Removed) + } + Some(_) => Ok(PluginDeregistrationOutcome::Replaced), + None => Ok(PluginDeregistrationOutcome::Missing), + } + } +} + +/// Owned registration for one provider in a host registry. +#[doc(hidden)] +pub struct InferenceProviderRegistration { + registry: InferenceProviderRegistry, + name: String, + registration_id: Option, +} + +impl InferenceProviderRegistration { + /// Returns the registered provider name. + pub fn name(&self) -> &str { + &self.name + } + + pub(crate) fn deregister_checked(&mut self) -> Result { + let Some(registration_id) = self.registration_id.take() else { + return Ok(PluginDeregistrationOutcome::Missing); + }; + self.registry.deregister(&self.name, registration_id) + } +} + +impl Drop for InferenceProviderRegistration { + fn drop(&mut self) { + if let Err(error) = self.deregister_checked() { + log::error!( + target: "nemo_relay.plugin", + event = "inference_provider_cleanup_failed", + provider = self.name.as_str(); + "Inference provider cleanup failed during drop: {error}" + ); + } + } +} + +fn normalized_identifier(value: String, field: &str) -> Result { + let normalized = value.trim(); + if normalized.is_empty() { + return Err(PluginError::RegistrationFailed(format!( + "{field} must not be empty" + ))); + } + Ok(normalized.to_string()) +} diff --git a/crates/core/src/plugin/local_model.rs b/crates/core/src/plugin/local_model.rs deleted file mode 100644 index 117839006..000000000 --- a/crates/core/src/plugin/local_model.rs +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Process-local model-provider registry used by first-party plugin components. - -use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, LazyLock, RwLock}; -use std::time::Duration; - -use serde_json::Value as Json; - -use super::{PluginDeregistrationOutcome, PluginError, Result}; - -/// JSON request-response provider backed by a local runtime or worker process. -#[doc(hidden)] -pub type LocalModelProviderFn = Arc Result + Send + Sync + 'static>; - -struct RegisteredLocalModelProvider { - registration_id: u64, - callback: LocalModelProviderFn, -} - -static LOCAL_MODEL_PROVIDERS: LazyLock>> = - LazyLock::new(|| RwLock::new(HashMap::new())); -static NEXT_LOCAL_MODEL_PROVIDER_ID: AtomicU64 = AtomicU64::new(1); - -/// Registers a named local-model provider and returns its ownership token. -#[doc(hidden)] -pub fn register_local_model_provider_tracked( - name: &str, - callback: LocalModelProviderFn, -) -> Result { - let name = name.trim(); - if name.is_empty() { - return Err(PluginError::RegistrationFailed( - "local-model provider name must not be empty".into(), - )); - } - let mut providers = LOCAL_MODEL_PROVIDERS.write().map_err(|error| { - PluginError::Internal(format!( - "local-model provider registry lock poisoned: {error}" - )) - })?; - if providers.contains_key(name) { - return Err(PluginError::RegistrationFailed(format!( - "local-model provider '{name}' is already registered" - ))); - } - let registration_id = NEXT_LOCAL_MODEL_PROVIDER_ID.fetch_add(1, Ordering::Relaxed); - providers.insert( - name.to_string(), - RegisteredLocalModelProvider { - registration_id, - callback, - }, - ); - Ok(registration_id) -} - -/// Resolves a named local-model provider. -#[doc(hidden)] -pub fn local_model_provider(name: &str) -> Result { - let name = name.trim(); - LOCAL_MODEL_PROVIDERS - .read() - .map_err(|error| { - PluginError::Internal(format!( - "local-model provider registry lock poisoned: {error}" - )) - })? - .get(name) - .map(|provider| Arc::clone(&provider.callback)) - .ok_or_else(|| { - PluginError::NotFound(format!("local-model provider '{name}' is not registered")) - }) -} - -/// Deregisters a local-model provider when the ownership token still matches. -#[doc(hidden)] -pub fn deregister_local_model_provider(name: &str, registration_id: u64) -> Result { - deregister_local_model_provider_checked(name, registration_id).map(|outcome| { - matches!( - outcome, - PluginDeregistrationOutcome::Removed | PluginDeregistrationOutcome::Missing - ) - }) -} - -pub(crate) fn deregister_local_model_provider_checked( - name: &str, - registration_id: u64, -) -> Result { - let name = name.trim(); - let mut providers = LOCAL_MODEL_PROVIDERS.write().map_err(|error| { - PluginError::Internal(format!( - "local-model provider registry lock poisoned: {error}" - )) - })?; - match providers.get(name) { - Some(provider) if provider.registration_id == registration_id => { - providers.remove(name); - Ok(PluginDeregistrationOutcome::Removed) - } - Some(_) => Ok(PluginDeregistrationOutcome::Replaced), - None => Ok(PluginDeregistrationOutcome::Missing), - } -} diff --git a/crates/core/tests/fixtures/worker_plugin/src/main.rs b/crates/core/tests/fixtures/worker_plugin/src/main.rs index 4322a8d08..99b3254ab 100644 --- a/crates/core/tests/fixtures/worker_plugin/src/main.rs +++ b/crates/core/tests/fixtures/worker_plugin/src/main.rs @@ -12,6 +12,8 @@ use serde_json::json; struct FixtureWorkerPlugin; +const DEFAULT_INFERENCE_CONTRACT: &str = "nemo.relay.pii_detection.v1"; + impl WorkerPlugin for FixtureWorkerPlugin { fn plugin_id(&self) -> &str { if std::env::var("FIXTURE_WORKER_PLUGIN_ID").as_deref() == Ok("other_worker") { @@ -57,8 +59,8 @@ impl WorkerPlugin for FixtureWorkerPlugin { ctx.register_subscriber("", |_| {}); return Ok(()); } - let local_model_provider_names = config - .get("local_model_provider_names") + let inference_provider_names = config + .get("inference_provider_names") .and_then(Json::as_array) .map(|names| { names @@ -70,16 +72,20 @@ impl WorkerPlugin for FixtureWorkerPlugin { .unwrap_or_else(|| { vec![ config - .get("local_model_provider_name") + .get("inference_provider_name") .and_then(Json::as_str) .unwrap_or("fixture_local_model") .to_string(), ] }); - for provider_name in local_model_provider_names { + for provider_name in inference_provider_names { let callback_provider_name = provider_name.clone(); let exit_in_local_model = fixture_flag(config, "exit_in_local_model"); - ctx.register_local_model_provider(&provider_name, move |request| { + let contract = config + .get("inference_provider_contract") + .and_then(Json::as_str) + .unwrap_or(DEFAULT_INFERENCE_CONTRACT); + ctx.register_inference_provider(&provider_name, contract, move |request| { let provider_name = callback_provider_name.clone(); async move { if exit_in_local_model { diff --git a/crates/core/tests/integration/worker_plugin_tests.rs b/crates/core/tests/integration/worker_plugin_tests.rs index 7bb6dee2d..b83940da2 100644 --- a/crates/core/tests/integration/worker_plugin_tests.rs +++ b/crates/core/tests/integration/worker_plugin_tests.rs @@ -24,18 +24,20 @@ use nemo_relay::codec::traits::LlmCodec; use nemo_relay::error::Result as FlowResult; use nemo_relay::plugin::dynamic::{ DynamicPluginActivationSpec, DynamicPluginKind, PluginHostActivation, WorkerPluginActivation, - WorkerPluginLoadSpec, load_worker_plugins, + WorkerPluginLoadSpec, load_worker_plugins, load_worker_plugins_with_inference_providers, }; use nemo_relay::plugin::{ - PluginComponentSpec, PluginConfig, clear_plugin_configuration, deregister_local_model_provider, - initialize_plugins_exact, list_plugin_kinds, local_model_provider, - register_local_model_provider_tracked, + InferenceProviderDescriptor, InferenceProviderRegistry, PluginComponentSpec, PluginConfig, + clear_plugin_configuration, initialize_plugins_exact, + initialize_plugins_exact_with_inference_providers, list_plugin_kinds, }; use serde_json::{Map, Value as Json, json}; use sha2::{Digest, Sha256}; use tempfile::TempDir; use uuid::Uuid; +const PII_DETECTION_CONTRACT: &str = "nemo.relay.pii_detection.v1"; +const EXAMPLE_ECHO_CONTRACT: &str = "examples.python_grpc_worker.echo.v1"; static WORKER_PLUGIN_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); fn enable_operational_logs() { @@ -52,7 +54,7 @@ fn worker_activation_with_no_specs_is_empty() { } #[tokio::test(flavor = "multi_thread")] -async fn worker_local_model_provider_is_preinstalled_times_out_and_clears() { +async fn worker_inference_provider_is_preinstalled_times_out_and_clears() { let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; let fixture = build_fixture_worker(); let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); @@ -63,33 +65,39 @@ async fn worker_local_model_provider_is_preinstalled_times_out_and_clears() { config: Map::new(), }]) .expect("worker plugin should load"); + let registry = activation.inference_providers(); // Providers must be available before static consumers initialize. - let provider = local_model_provider("fixture_worker/fixture_local_model") + let provider = registry + .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) .expect("worker provider should be installed"); assert_eq!( - provider( - json!({"text": "private"}), - std::time::Duration::from_secs(1) - ) - .expect("worker provider should return JSON"), + provider + .invoke( + json!({"text": "private"}), + std::time::Duration::from_secs(1) + ) + .expect("worker provider should return JSON"), json!({ "version": 1, "request": {"text": "private"}, "provider": "fixture_local_model" }) ); - let timeout = provider( - json!({"delay_ms": 100}), - std::time::Duration::from_millis(5), - ) - .expect_err("worker provider should honor the caller deadline") - .to_string(); + let timeout = provider + .invoke( + json!({"delay_ms": 100}), + std::time::Duration::from_millis(5), + ) + .expect_err("worker provider should honor the caller deadline") + .to_string(); assert!(timeout.contains("timed out"), "{timeout}"); activation.clear(); assert!( - local_model_provider("fixture_worker/fixture_local_model").is_err(), + registry + .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) + .is_err(), "provider should be removed when the worker activation clears" ); } @@ -106,10 +114,12 @@ async fn worker_clear_fails_an_in_flight_local_model_call_without_hanging() { config: Map::new(), }]) .expect("worker plugin should load"); - let provider = local_model_provider("fixture_worker/fixture_local_model") + let registry = activation.inference_providers(); + let provider = registry + .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) .expect("worker provider should be installed"); let invocation = std::thread::spawn(move || { - provider( + provider.invoke( json!({"delay_ms": 5_000}), std::time::Duration::from_secs(10), ) @@ -128,7 +138,9 @@ async fn worker_clear_fails_an_in_flight_local_model_call_without_hanging() { "{error}" ); assert!( - local_model_provider("fixture_worker/fixture_local_model").is_err(), + registry + .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) + .is_err(), "provider should remain deregistered after concurrent clear" ); } @@ -140,37 +152,52 @@ async fn worker_provider_rolls_back_after_later_plugin_registration_failure() { let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); let first_provider = "fixture_local_model_first"; let first_provider_key = format!("fixture_worker/{first_provider}"); - let first = load_worker_plugins([WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([("local_model_provider_name".into(), json!(first_provider))]), - }]) + let registry = InferenceProviderRegistry::default(); + let first = load_worker_plugins_with_inference_providers( + [WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([("inference_provider_name".into(), json!(first_provider))]), + }], + registry.clone(), + ) .expect("first worker plugin should load"); let second_provider = "fixture_local_model_rollback"; let second_provider_key = format!("fixture_worker/{second_provider}"); - let second = load_worker_plugins([WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([("local_model_provider_name".into(), json!(second_provider))]), - }]); + let second = load_worker_plugins_with_inference_providers( + [WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([("inference_provider_name".into(), json!(second_provider))]), + }], + registry.clone(), + ); assert!( second.is_err(), "duplicate plugin kind should fail after the second provider is installed" ); assert!( - local_model_provider(&second_provider_key).is_err(), + registry + .resolve(&second_provider_key, PII_DETECTION_CONTRACT) + .is_err(), "the second provider must be rolled back with its failed activation" ); assert!( - local_model_provider(&first_provider_key).is_ok(), + registry + .resolve(&first_provider_key, PII_DETECTION_CONTRACT) + .is_ok(), "rollback must not remove the first activation's provider" ); first.clear(); - assert!(local_model_provider(&first_provider_key).is_err()); + assert!( + registry + .resolve(&first_provider_key, PII_DETECTION_CONTRACT) + .is_err() + ); } #[tokio::test(flavor = "multi_thread")] @@ -180,41 +207,47 @@ async fn worker_provider_rolls_back_earlier_provider_after_same_worker_conflict( let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); let first_provider_key = "fixture_worker/fixture_local_model_unique"; let conflicting_provider_key = "fixture_worker/fixture_local_model_conflict"; - let existing_registration = register_local_model_provider_tracked( - conflicting_provider_key, - Arc::new(|request, _| Ok(json!({"existing": request}))), - ) - .expect("conflicting provider fixture should register"); + let registry = InferenceProviderRegistry::default(); + let _existing_registration = registry + .register( + InferenceProviderDescriptor::new(conflicting_provider_key, PII_DETECTION_CONTRACT) + .unwrap(), + Arc::new(|request, _| Ok(json!({"existing": request}))), + ) + .expect("conflicting provider fixture should register"); - let activation = load_worker_plugins([WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([( - "local_model_provider_names".into(), - json!(["fixture_local_model_unique", "fixture_local_model_conflict"]), - )]), - }]); + let activation = load_worker_plugins_with_inference_providers( + [WorkerPluginLoadSpec { + plugin_id: "fixture_worker".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + environment_ref: None, + config: Map::from_iter([( + "inference_provider_names".into(), + json!(["fixture_local_model_unique", "fixture_local_model_conflict"]), + )]), + }], + registry.clone(), + ); assert!( activation.is_err(), "the worker activation should fail on its second provider" ); assert!( - local_model_provider(first_provider_key).is_err(), + registry + .resolve(first_provider_key, PII_DETECTION_CONTRACT) + .is_err(), "an earlier provider from the failed worker must be rolled back" ); - let existing = local_model_provider(conflicting_provider_key) + let existing = registry + .resolve(conflicting_provider_key, PII_DETECTION_CONTRACT) .expect("the existing conflicting provider must remain registered"); assert_eq!( - existing(json!({"value": 1}), std::time::Duration::from_secs(1)) + existing + .invoke(json!({"value": 1}), std::time::Duration::from_secs(1)) .expect("existing provider should remain callable"), json!({"existing": {"value": 1}}) ); - assert!( - deregister_local_model_provider(conflicting_provider_key, existing_registration) - .expect("existing provider should deregister") - ); } #[tokio::test] @@ -1264,6 +1297,7 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { config: config.clone(), }]) .expect("managed Python worker should load"); + let inference_providers = activation.inference_providers(); let mut cleanup = PythonWorkerCleanup::new(activation); let mut plugin_config = PluginConfig::default(); @@ -1272,7 +1306,7 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { enabled: true, config, }); - initialize_plugins_exact(plugin_config) + initialize_plugins_exact_with_inference_providers(plugin_config, inference_providers.clone()) .await .expect("managed Python worker should initialize"); @@ -1292,14 +1326,16 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { rewritten["_nemo_relay_plugin"]["tag"], "managed-environment" ); - let local_model = local_model_provider("examples.python_grpc_worker/echo") - .expect("Python worker should expose its local-model provider"); + let inference_provider = inference_providers + .resolve("examples.python_grpc_worker/echo", EXAMPLE_ECHO_CONTRACT) + .expect("Python worker should expose its inference provider"); assert_eq!( - local_model( - json!({"version": 1, "texts": [{"id": 0, "text": "private"}]}), - std::time::Duration::from_secs(1), - ) - .expect("Python local-model provider should round-trip JSON"), + inference_provider + .invoke( + json!({"version": 1, "texts": [{"id": 0, "text": "private"}]}), + std::time::Duration::from_secs(1), + ) + .expect("Python inference provider should round-trip JSON"), json!({ "provider": "python_grpc_worker", "request": { @@ -1419,6 +1455,7 @@ async fn load_and_initialize_fixture(config: Map) -> LoadedWorker config: config.clone(), }]) .expect("worker plugin should load"); + let inference_providers = activation.inference_providers(); let mut plugin_config = PluginConfig::default(); plugin_config.components.push(PluginComponentSpec { @@ -1426,7 +1463,7 @@ async fn load_and_initialize_fixture(config: Map) -> LoadedWorker enabled: true, config, }); - initialize_plugins_exact(plugin_config) + initialize_plugins_exact_with_inference_providers(plugin_config, inference_providers) .await .expect("worker plugin should initialize"); diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index 7754238dc..0d250926d 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -246,6 +246,7 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { surface: RegistrationSurface::Subscriber as i32, priority: 0, break_chain: false, + contract: String::new(), }], error: None, }, @@ -261,6 +262,7 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { surface: 999, priority: 0, break_chain: false, + contract: String::new(), }], error: None, }, @@ -280,6 +282,7 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { surface: RegistrationSurface::Unspecified as i32, priority: 0, break_chain: false, + contract: String::new(), }], error: None, }, @@ -291,6 +294,44 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { .contains("unspecified registration surface") ); + let missing_contract = validate_registration_plan( + "fixture_worker", + &RegisterResponse { + registrations: vec![registration( + RegistrationSurface::InferenceProvider, + "detector", + )], + error: None, + }, + ) + .expect_err("inference providers must declare a contract"); + assert!(missing_contract.to_string().contains("without a contract")); + + let contract_on_middleware = validate_registration_plan( + "fixture_worker", + &RegisterResponse { + registrations: vec![Registration { + contract: "test.detector.v1".into(), + ..registration(RegistrationSurface::Subscriber, "subscriber") + }], + error: None, + }, + ) + .expect_err("middleware registrations must not declare provider contracts"); + assert!(contract_on_middleware.to_string().contains("non-provider")); + + validate_registration_plan( + "fixture_worker", + &RegisterResponse { + registrations: vec![Registration { + contract: "test.detector.v1".into(), + ..registration(RegistrationSurface::InferenceProvider, "detector") + }], + error: None, + }, + ) + .expect("versioned inference provider contract should be accepted"); + let cases = [ (ProtoScopeType::Agent, crate::api::scope::ScopeType::Agent), ( @@ -2148,6 +2189,7 @@ fn registration(surface: RegistrationSurface, local_name: &str) -> Registration surface: surface as i32, priority: 0, break_chain: false, + contract: String::new(), } } diff --git a/crates/core/tests/unit/inference_provider_tests.rs b/crates/core/tests/unit/inference_provider_tests.rs new file mode 100644 index 000000000..5e9ccc5e7 --- /dev/null +++ b/crates/core/tests/unit/inference_provider_tests.rs @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; +use std::time::Duration; + +use serde_json::json; + +use crate::plugin::{InferenceProviderDescriptor, InferenceProviderRegistry}; + +#[test] +fn provider_round_trips_json_and_receives_deadline() { + let registry = InferenceProviderRegistry::default(); + let _registration = registry + .register( + InferenceProviderDescriptor::new("test-provider", "test.echo.v1").unwrap(), + Arc::new(|request, timeout| { + assert_eq!(timeout, Duration::from_millis(25)); + Ok(json!({"request": request})) + }), + ) + .unwrap(); + + let provider = registry.resolve("test-provider", "test.echo.v1").unwrap(); + assert_eq!( + provider + .invoke(json!({"text": "hello"}), Duration::from_millis(25)) + .unwrap(), + json!({"request": {"text": "hello"}}) + ); +} + +#[test] +fn registration_owns_provider_lifetime() { + let registry = InferenceProviderRegistry::default(); + let registration = registry + .register( + InferenceProviderDescriptor::new("owned-provider", "test.echo.v1").unwrap(), + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + + assert!(registry.resolve("owned-provider", "test.echo.v1").is_ok()); + drop(registration); + assert!(registry.resolve("owned-provider", "test.echo.v1").is_err()); +} + +#[test] +fn duplicate_provider_names_are_rejected() { + let registry = InferenceProviderRegistry::default(); + let _registration = registry + .register( + InferenceProviderDescriptor::new("duplicate-provider", "test.echo.v1").unwrap(), + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + let duplicate = registry + .register( + InferenceProviderDescriptor::new("duplicate-provider", "test.other.v1").unwrap(), + Arc::new(|request, _| Ok(request)), + ) + .err() + .expect("duplicate provider names must fail"); + + assert!(duplicate.to_string().contains("already registered")); +} + +#[test] +fn provider_names_are_normalized_consistently() { + let registry = InferenceProviderRegistry::default(); + let _registration = registry + .register( + InferenceProviderDescriptor::new(" normalized-provider ", " test.echo.v1 ") + .unwrap(), + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + + assert!( + registry + .resolve(" normalized-provider ", " test.echo.v1 ") + .is_ok() + ); +} + +#[test] +fn provider_contract_mismatch_is_rejected_before_invocation() { + let registry = InferenceProviderRegistry::default(); + let _registration = registry + .register( + InferenceProviderDescriptor::new("detector", "test.detector.v1").unwrap(), + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + + let error = registry + .resolve("detector", "test.embedding.v1") + .err() + .expect("mismatched contracts must fail"); + assert!(error.to_string().contains("test.detector.v1")); + assert!(error.to_string().contains("test.embedding.v1")); +} + +#[test] +fn registries_isolate_provider_names_between_hosts() { + let first = InferenceProviderRegistry::default(); + let second = InferenceProviderRegistry::default(); + let _first_registration = first + .register( + InferenceProviderDescriptor::new("shared-name", "test.echo.v1").unwrap(), + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + + assert!(first.resolve("shared-name", "test.echo.v1").is_ok()); + assert!(second.resolve("shared-name", "test.echo.v1").is_err()); +} diff --git a/crates/core/tests/unit/local_model_tests.rs b/crates/core/tests/unit/local_model_tests.rs deleted file mode 100644 index 871033d92..000000000 --- a/crates/core/tests/unit/local_model_tests.rs +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::Arc; -use std::time::Duration; - -use serde_json::json; - -use crate::plugin::{ - deregister_local_model_provider, local_model_provider, register_local_model_provider_tracked, -}; - -#[test] -fn provider_round_trips_json_and_receives_deadline() { - let registration_id = register_local_model_provider_tracked( - "test-provider", - Arc::new(|request, timeout| { - assert_eq!(timeout, Duration::from_millis(25)); - Ok(json!({"request": request})) - }), - ) - .unwrap(); - - let provider = local_model_provider("test-provider").unwrap(); - assert_eq!( - provider(json!({"text": "hello"}), Duration::from_millis(25)).unwrap(), - json!({"request": {"text": "hello"}}) - ); - assert!(deregister_local_model_provider("test-provider", registration_id).unwrap()); -} - -#[test] -fn ownership_token_does_not_remove_another_registration() { - let registration_id = - register_local_model_provider_tracked("owned-provider", Arc::new(|request, _| Ok(request))) - .unwrap(); - - assert!(!deregister_local_model_provider("owned-provider", registration_id + 1).unwrap()); - assert!(local_model_provider("owned-provider").is_ok()); - assert!(deregister_local_model_provider("owned-provider", registration_id).unwrap()); -} - -#[test] -fn duplicate_provider_names_are_rejected() { - let registration_id = register_local_model_provider_tracked( - "duplicate-provider", - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - let duplicate = register_local_model_provider_tracked( - "duplicate-provider", - Arc::new(|request, _| Ok(request)), - ) - .unwrap_err(); - - assert!(duplicate.to_string().contains("already registered")); - assert!(deregister_local_model_provider("duplicate-provider", registration_id).unwrap()); -} - -#[test] -fn provider_names_are_normalized_consistently() { - let registration_id = register_local_model_provider_tracked( - " normalized-provider ", - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - - assert!(local_model_provider(" normalized-provider ").is_ok()); - assert!(deregister_local_model_provider(" normalized-provider ", registration_id).unwrap()); -} diff --git a/crates/core/tests/unit/plugin_tests.rs b/crates/core/tests/unit/plugin_tests.rs index b39abb9d2..2cab29057 100644 --- a/crates/core/tests/unit/plugin_tests.rs +++ b/crates/core/tests/unit/plugin_tests.rs @@ -26,6 +26,7 @@ struct RecordingPlugin; struct ReplacementPlugin; struct RestoreFailPlugin; struct RestoreBreakPlugin; +struct ProviderAwarePlugin; struct PartialFailPlugin; struct VanishingPlugin; struct BlockingPlugin { @@ -55,11 +56,16 @@ static PARTIAL_FAIL_ROLLBACKS: AtomicUsize = AtomicUsize::new(0); static RESTORE_FAIL_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); static RESTORE_BREAK_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); static REPLACEMENT_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); +static INFERENCE_PROVIDER_VALUES: OnceLock>> = OnceLock::new(); fn recorded_names() -> &'static Mutex> { RECORDED_NAMES.get_or_init(|| Mutex::new(Vec::new())) } +fn inference_provider_values() -> &'static Mutex> { + INFERENCE_PROVIDER_VALUES.get_or_init(|| Mutex::new(Vec::new())) +} + fn lock_runtime_owner() -> std::sync::MutexGuard<'static, ()> { crate::shared_runtime::runtime_owner_test_mutex() .lock() @@ -305,6 +311,40 @@ impl Plugin for RestoreBreakPlugin { } } +impl Plugin for ProviderAwarePlugin { + fn plugin_kind(&self) -> &str { + "provider-aware.plugin" + } + + fn validate(&self, _plugin_config: &Map) -> Vec { + vec![] + } + + fn register<'a>( + &'a self, + _plugin_config: &Map, + ctx: &'a mut PluginRegistrationContext, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let provider = ctx.inference_provider("shared-provider", "test.echo.v1")?; + let response = provider.invoke(json!({}), std::time::Duration::from_secs(1))?; + let source = response + .get("source") + .and_then(Json::as_str) + .ok_or_else(|| { + PluginError::RegistrationFailed( + "provider-aware.plugin received an invalid response".into(), + ) + })?; + inference_provider_values() + .lock() + .unwrap() + .push(source.to_string()); + Ok(()) + }) + } +} + impl Plugin for PartialFailPlugin { fn plugin_kind(&self) -> &str { "partial.fail.plugin" @@ -477,12 +517,14 @@ fn reset_global() { RESTORE_FAIL_REGISTRATIONS.store(0, Ordering::SeqCst); RESTORE_BREAK_REGISTRATIONS.store(0, Ordering::SeqCst); REPLACEMENT_REGISTRATIONS.store(0, Ordering::SeqCst); + inference_provider_values().lock().unwrap().clear(); let _ = deregister_plugin("test.plugin"); let _ = deregister_plugin("singleton.plugin"); let _ = deregister_plugin("recording.plugin"); let _ = deregister_plugin("replacement.plugin"); let _ = deregister_plugin("restore.fail.plugin"); let _ = deregister_plugin("restore.break.plugin"); + let _ = deregister_plugin("provider-aware.plugin"); let _ = deregister_plugin("partial.fail.plugin"); let _ = deregister_plugin("vanishing.plugin"); let _ = deregister_plugin("blocking.plugin"); @@ -1110,6 +1152,60 @@ fn test_initialize_plugins_restores_previous_configuration_after_failed_replacem reset_global(); } +#[test] +fn test_failed_replacement_restores_previous_inference_provider_registry() { + let _guard = lock_runtime_owner(); + reset_global(); + register_plugin(Arc::new(ProviderAwarePlugin)).unwrap(); + register_plugin(Arc::new(RestoreFailPlugin)).unwrap(); + + let previous_registry = InferenceProviderRegistry::default(); + let _previous_provider = previous_registry + .register( + InferenceProviderDescriptor::new("shared-provider", "test.echo.v1").unwrap(), + Arc::new(|_, _| Ok(json!({"source": "previous"}))), + ) + .unwrap(); + let replacement_registry = InferenceProviderRegistry::default(); + let _replacement_provider = replacement_registry + .register( + InferenceProviderDescriptor::new("shared-provider", "test.echo.v1").unwrap(), + Arc::new(|_, _| Ok(json!({"source": "replacement"}))), + ) + .unwrap(); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime + .block_on(initialize_plugins_exact_with_inference_providers( + PluginConfig { + components: vec![PluginComponentSpec::new("provider-aware.plugin")], + ..PluginConfig::default() + }, + previous_registry, + )) + .unwrap(); + + let error = runtime + .block_on(initialize_plugins_exact_with_inference_providers( + PluginConfig { + components: vec![PluginComponentSpec::new("restore.fail.plugin")], + ..PluginConfig::default() + }, + replacement_registry, + )) + .unwrap_err(); + assert!(error.to_string().contains("refused to initialize")); + assert_eq!( + *inference_provider_values().lock().unwrap(), + vec!["previous", "previous"] + ); + + reset_global(); +} + #[test] fn test_initialize_plugins_restores_previous_configuration_after_replacement_panic() { let _guard = lock_runtime_owner(); @@ -1318,6 +1414,7 @@ fn test_checked_teardown_reports_unremoved_registrations() { )) }), )], + InferenceProviderRegistry::default(), ) .unwrap(); @@ -1343,6 +1440,7 @@ fn test_legacy_clear_retains_mutation_owner_after_incomplete_teardown() { "stale-callback", Box::new(|| panic!("fixture deregistration panicked")), )], + InferenceProviderRegistry::default(), ) .unwrap(); diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index 3ef479846..a536d8649 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -223,9 +223,10 @@ max_latency_ms = 250 The backend name is `/`. For example, a worker with plugin ID `acme.pii_worker` that calls -`register_local_model_provider("detector", ...)` is selected as -`acme.pii_worker/detector`. Relay installs worker providers before static -components initialize and removes PII sanitizers before stopping their worker. +`register_inference_provider("detector", "nemo.relay.pii_detection.v1", ...)` +is selected as `acme.pii_worker/detector`. Relay verifies the PII contract, +installs worker providers before static components initialize, and removes PII +sanitizers before stopping their worker. Use profiles to compose deterministic and contextual detection. The lower priority runs first: @@ -331,8 +332,9 @@ The source tree includes an optional [Rampart worker](./providers/rampart/README.md) that implements this provider contract with a pinned ONNX token-classification model. It runs in a Relay-managed Python worker process, keeps ONNX dependencies out of the host, -and complements the built-in deterministic recognizers. The model is acquired -at activation time and is not distributed in the Relay package. +and complements the built-in deterministic recognizers. The model is prefetched +separately, then resolved and integrity-verified from the local cache at +activation. It is not distributed in the Relay package. ## Documentation diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py index 71f7c3e97..027d78593 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Rampart local-model provider for the NeMo Relay PII component.""" +"""Rampart inference provider for the NeMo Relay PII component.""" from .detector import DEFAULT_MODEL_ID, DEFAULT_MODEL_REVISION, RampartDetector, RampartSettings diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py index d1eb58e01..bd62e814d 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Manifest entrypoint for the Rampart PII local-model provider.""" +"""Manifest entrypoint for the Rampart PII inference provider.""" from __future__ import annotations @@ -12,6 +12,8 @@ from .detector import RampartDetector, RampartSettings, resolve_verified_model_root +PII_DETECTION_PROVIDER_CONTRACT = "nemo.relay.pii_detection.v1" + class _Admission: def __init__(self, limit: int) -> None: @@ -87,7 +89,11 @@ def release_after_work(_task: asyncio.Task[Json]) -> None: if not release_on_completion: admission.release() - ctx.register_local_model_provider("detector", detect) + ctx.register_inference_provider( + "detector", + PII_DETECTION_PROVIDER_CONTRACT, + detect, + ) async def main() -> None: diff --git a/crates/pii-redaction/providers/rampart/pyproject.toml b/crates/pii-redaction/providers/rampart/pyproject.toml index 62bc82787..c658534cb 100644 --- a/crates/pii-redaction/providers/rampart/pyproject.toml +++ b/crates/pii-redaction/providers/rampart/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-relay-pii-rampart" version = "0.1.0" -description = "Optional Rampart local-model provider for NeMo Relay PII redaction" +description = "Optional Rampart inference provider for NeMo Relay PII redaction" readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0" diff --git a/crates/pii-redaction/providers/rampart/tests/test_worker.py b/crates/pii-redaction/providers/rampart/tests/test_worker.py index d703a61ce..7c590f402 100644 --- a/crates/pii-redaction/providers/rampart/tests/test_worker.py +++ b/crates/pii-redaction/providers/rampart/tests/test_worker.py @@ -18,8 +18,9 @@ class FakeContext: def __init__(self) -> None: self.callback: Any = None - def register_local_model_provider(self, name: str, callback: Any) -> None: + def register_inference_provider(self, name: str, contract: str, callback: Any) -> None: assert name == "detector" + assert contract == "nemo.relay.pii_detection.v1" self.callback = callback diff --git a/crates/pii-redaction/src/component.rs b/crates/pii-redaction/src/component.rs index 8f574b507..3165e4a25 100644 --- a/crates/pii-redaction/src/component.rs +++ b/crates/pii-redaction/src/component.rs @@ -28,6 +28,8 @@ use super::local::{register_local_backend, validate_local_backend_config}; /// The plugin kind reserved for the built-in privacy component. pub const PII_REDACTION_PLUGIN_KIND: &str = "pii_redaction"; +/// Versioned inference contract implemented by PII detection providers. +pub const PII_DETECTION_PROVIDER_CONTRACT: &str = "nemo.relay.pii_detection.v1"; pub(super) const DEFAULT_LOCAL_MODEL_LATENCY_MS: u64 = 250; pub(super) const DEFAULT_LOCAL_MODEL_MIN_SCORE: f64 = 0.4; pub(super) const MAX_LOCAL_MODEL_LATENCY_MS: u64 = 60_000; diff --git a/crates/pii-redaction/src/local.rs b/crates/pii-redaction/src/local.rs index 23d1f0651..5ba225764 100644 --- a/crates/pii-redaction/src/local.rs +++ b/crates/pii-redaction/src/local.rs @@ -15,8 +15,7 @@ use nemo_relay::codec::resolve::{ }; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_relay::plugin::{ - LocalModelProviderFn, PluginError, PluginRegistrationContext, Result as PluginResult, - local_model_provider, + InferenceProvider, PluginError, PluginRegistrationContext, Result as PluginResult, }; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -26,8 +25,9 @@ use super::component::{ DEFAULT_LOCAL_MODEL_LATENCY_MS, DEFAULT_LOCAL_MODEL_MIN_SCORE, LocalBackendConfig, MAX_LOCAL_MODEL_EXCLUDED_LABELS, MAX_LOCAL_MODEL_LABEL_BYTES, MAX_LOCAL_MODEL_LATENCY_MS, MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES, MAX_LOCAL_MODEL_REPLACEMENT_BYTES, - MAX_LOCAL_MODEL_TARGET_PATH_BYTES, MAX_LOCAL_MODEL_TARGET_PATHS, PiiRedactionConfig, - is_valid_json_pointer, is_valid_json_pointer_pattern, profile_registration_prefix, + MAX_LOCAL_MODEL_TARGET_PATH_BYTES, MAX_LOCAL_MODEL_TARGET_PATHS, + PII_DETECTION_PROVIDER_CONTRACT, PiiRedactionConfig, is_valid_json_pointer, + is_valid_json_pointer_pattern, profile_registration_prefix, }; use super::overlay::BuiltinCodecName; @@ -42,7 +42,7 @@ const MAX_DETECTIONS_PER_TEXT: usize = 128; #[derive(Clone)] struct CompiledLocalBackend { provider_name: Arc, - provider: LocalModelProviderFn, + provider: InferenceProvider, model_id: Option, detector_profile: Option, target_paths: Arc>, @@ -118,7 +118,11 @@ struct SelectedText { } impl CompiledLocalBackend { - fn new(config: LocalBackendConfig, codec_name: Option) -> PluginResult { + fn new( + config: LocalBackendConfig, + codec_name: Option, + ctx: &PluginRegistrationContext, + ) -> PluginResult { if let Some(violation) = validate_local_backend_config(&config).into_iter().next() { return Err(PluginError::InvalidConfig(violation.message)); } @@ -141,11 +145,13 @@ impl CompiledLocalBackend { })?), None => None, }; - let provider = local_model_provider(&provider_name).map_err(|_| { - PluginError::RegistrationFailed(format!( - "PII redaction local-model provider '{provider_name}' is unavailable" - )) - })?; + let provider = ctx + .inference_provider(&provider_name, PII_DETECTION_PROVIDER_CONTRACT) + .map_err(|error| { + PluginError::RegistrationFailed(format!( + "PII redaction inference provider '{provider_name}' is unavailable: {error}" + )) + })?; Ok(Self { provider_name: Arc::new(provider_name), provider, @@ -364,7 +370,7 @@ impl CompiledLocalBackend { .collect(), }; let request = serde_json::to_value(request)?; - let response = (self.provider)(request, timeout)?; + let response = self.provider.invoke(request, timeout)?; let response: LocalModelResponse = serde_json::from_value(response).map_err(|error| { PluginError::RegistrationFailed(format!( "local-model provider returned an invalid detection response: {error}" @@ -496,7 +502,7 @@ pub(super) fn register_local_backend( "local settings are required when mode = 'local_model'".to_string(), ) })?; - let backend = CompiledLocalBackend::new(local, config.codec.clone())?; + let backend = CompiledLocalBackend::new(local, config.codec.clone(), ctx)?; if config.mark { ctx.register_mark_sanitize_guardrail( diff --git a/crates/pii-redaction/tests/unit/component_tests.rs b/crates/pii-redaction/tests/unit/component_tests.rs index 6f25a970c..a316732e4 100644 --- a/crates/pii-redaction/tests/unit/component_tests.rs +++ b/crates/pii-redaction/tests/unit/component_tests.rs @@ -28,11 +28,11 @@ use crate::codec::openai_responses::OpenAIResponsesCodec; use crate::codec::request::AnnotatedLlmRequest; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::plugin::{ - ConfigPolicy, DiagnosticLevel, PluginComponentSpec, PluginConfig, PluginError, + ConfigPolicy, DiagnosticLevel, InferenceProviderDescriptor, InferenceProviderRegistration, + InferenceProviderRegistry, PluginComponentSpec, PluginConfig, PluginError, PluginRegistrationContext, UnsupportedBehavior, clear_plugin_configuration, - deregister_local_model_provider, ensure_builtin_plugins_registered, initialize_plugins_exact as initialize_plugins, - list_plugin_kinds, register_local_model_provider_tracked, rollback_registrations, + initialize_plugins_with_inference_providers, list_plugin_kinds, rollback_registrations, validate_plugin_config, }; use futures::StreamExt; @@ -148,25 +148,23 @@ fn reset_runtime() { register_pii_redaction_component().unwrap(); } -struct LocalModelProviderGuard { - name: String, - registration_id: u64, +struct InferenceProviderGuard { + _registration: InferenceProviderRegistration, } -impl Drop for LocalModelProviderGuard { - fn drop(&mut self) { - let _ = deregister_local_model_provider(&self.name, self.registration_id); - } -} - -fn register_test_local_model_provider( +fn register_test_inference_provider( + registry: &InferenceProviderRegistry, name: &str, callback: impl Fn(Json, std::time::Duration) -> Result + Send + Sync + 'static, -) -> LocalModelProviderGuard { - let registration_id = register_local_model_provider_tracked(name, Arc::new(callback)).unwrap(); - LocalModelProviderGuard { - name: name.to_string(), - registration_id, +) -> InferenceProviderGuard { + let registration = registry + .register( + InferenceProviderDescriptor::new(name, PII_DETECTION_PROVIDER_CONTRACT).unwrap(), + Arc::new(callback), + ) + .unwrap(); + InferenceProviderGuard { + _registration: registration, } } @@ -1286,24 +1284,27 @@ fn deterministic_and_local_model_profiles_compose_in_priority_order() { reset_runtime(); setup_isolated_thread(); - let _provider = register_test_local_model_provider("contextual", |request, _| { - assert_eq!( - request["texts"][0]["text"], "Alice emailed [REDACTED]", - "the local provider must receive the deterministic profile's output" - ); - Ok(json!({ - "version": 1, - "detections": [{ - "text_id": 0, - "start_utf8": 0, - "end_utf8": 5, - "label": "GIVEN_NAME", - "score": 0.99 - }] - })) - }); + let inference_providers = InferenceProviderRegistry::default(); + let _provider = + register_test_inference_provider(&inference_providers, "contextual", |request, _| { + assert_eq!( + request["texts"][0]["text"], "Alice emailed [REDACTED]", + "the local provider must receive the deterministic profile's output" + ); + Ok(json!({ + "version": 1, + "detections": [{ + "text_id": 0, + "start_utf8": 0, + "end_utf8": 5, + "label": "GIVEN_NAME", + "score": 0.99 + }] + })) + }); - futures::executor::block_on(initialize_plugins(plugin_config(json!({ + futures::executor::block_on(initialize_plugins_with_inference_providers( + plugin_config(json!({ "codec": "openai_chat", "profiles": [ { @@ -1323,7 +1324,9 @@ fn deterministic_and_local_model_profiles_compose_in_priority_order() { } } ] - })))) + })), + inference_providers, + )) .unwrap(); let events = capture_events("pii-profile-composition"); @@ -1457,10 +1460,11 @@ fn local_profile_registrations_receive_generated_namespaces() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); - let _one = register_test_local_model_provider("one", |_, _| { + let inference_providers = InferenceProviderRegistry::default(); + let _one = register_test_inference_provider(&inference_providers, "one", |_, _| { Ok(json!({"version": 1, "detections": []})) }); - let _two = register_test_local_model_provider("two", |_, _| { + let _two = register_test_inference_provider(&inference_providers, "two", |_, _| { Ok(json!({"version": 1, "detections": []})) }); @@ -1475,7 +1479,10 @@ fn local_profile_registrations_receive_generated_namespaces() { let Json::Object(config) = config else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_namespace("profiles::"); + let mut ctx = PluginRegistrationContext::with_inference_providers( + Some("profiles::".into()), + inference_providers, + ); futures::executor::block_on(plugin.register(&config, &mut ctx)).unwrap(); let mut registrations = ctx.into_registrations(); let registrations_debug = format!("{registrations:?}"); @@ -2027,13 +2034,19 @@ fn local_backend_provider_is_invoked_for_local_model_mode() { let called = Arc::new(AtomicBool::new(false)); let called_inner = Arc::clone(&called); - let _provider = register_test_local_model_provider("test-provider", move |request, _| { - called_inner.store(true, Ordering::SeqCst); - assert_eq!(request["version"], 1); - Ok(json!({"version": 1, "detections": []})) - }); + let inference_providers = InferenceProviderRegistry::default(); + let _provider = register_test_inference_provider( + &inference_providers, + "test-provider", + move |request, _| { + called_inner.store(true, Ordering::SeqCst); + assert_eq!(request["version"], 1); + Ok(json!({"version": 1, "detections": []})) + }, + ); setup_isolated_thread(); - futures::executor::block_on(initialize_plugins(plugin_config(json!({ + futures::executor::block_on(initialize_plugins_with_inference_providers( + plugin_config(json!({ "mode": "local_model", "input": false, "output": false, @@ -2041,7 +2054,9 @@ fn local_backend_provider_is_invoked_for_local_model_mode() { "tool_input": true, "tool_output": false, "local": {"backend": "test-provider"} - })))) + })), + inference_providers, + )) .unwrap(); tool_call( ToolCallParams::builder() @@ -2077,7 +2092,8 @@ fn local_backend_reports_missing_and_failed_provider_initialization() { .expect_err("missing local provider should fail registration"); assert!(missing.to_string().contains("unavailable")); - let _failed = register_test_local_model_provider("failed", |_, _| { + let inference_providers = InferenceProviderRegistry::default(); + let _failed = register_test_inference_provider(&inference_providers, "failed", |_, _| { Err(PluginError::RegistrationFailed("provider failed".into())) }); let config = json!({ @@ -2092,13 +2108,52 @@ fn local_backend_reports_missing_and_failed_provider_initialization() { let Json::Object(config) = config else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_namespace("failed::"); + let mut ctx = PluginRegistrationContext::with_inference_providers( + Some("failed::".into()), + inference_providers, + ); futures::executor::block_on(plugin.register(&config, &mut ctx)) .expect("provider availability should be checked at registration"); let mut registrations = ctx.into_registrations(); rollback_registrations(&mut registrations); } +#[test] +fn local_backend_rejects_provider_with_incompatible_contract() { + let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); + reset_runtime(); + + let inference_providers = InferenceProviderRegistry::default(); + let _registration = inference_providers + .register( + InferenceProviderDescriptor::new("embedding", "acme.embedding.v1").unwrap(), + Arc::new(|request, _| Ok(request)), + ) + .unwrap(); + let plugin = PiiRedactionPlugin; + let Json::Object(config) = json!({ + "mode": "local_model", + "input": false, + "output": false, + "mark": false, + "tool_input": true, + "tool_output": false, + "local": {"backend": "embedding"} + }) else { + panic!("component config must be object"); + }; + let mut ctx = PluginRegistrationContext::with_inference_providers( + Some("mismatch::".into()), + inference_providers, + ); + + let error = futures::executor::block_on(plugin.register(&config, &mut ctx)) + .expect_err("PII must reject providers implementing another contract"); + + assert!(error.to_string().contains("acme.embedding.v1")); + assert!(error.to_string().contains(PII_DETECTION_PROVIDER_CONTRACT)); +} + #[test] fn builtin_backend_sanitizes_mark_and_generic_scope_observability_fields() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); diff --git a/crates/pii-redaction/tests/unit/local_tests.rs b/crates/pii-redaction/tests/unit/local_tests.rs index 6fd2db179..da3935c8c 100644 --- a/crates/pii-redaction/tests/unit/local_tests.rs +++ b/crates/pii-redaction/tests/unit/local_tests.rs @@ -3,42 +3,52 @@ use std::sync::atomic::{AtomicUsize, Ordering}; -use nemo_relay::plugin::{deregister_local_model_provider, register_local_model_provider_tracked}; +use nemo_relay::plugin::{ + InferenceProviderDescriptor, InferenceProviderRegistration, InferenceProviderRegistry, + PluginRegistrationContext, +}; use serde_json::json; use super::*; struct ProviderGuard { - name: &'static str, - registration_id: u64, + _registration: InferenceProviderRegistration, } -impl Drop for ProviderGuard { - fn drop(&mut self) { - let _ = deregister_local_model_provider(self.name, self.registration_id); - } +fn provider_context( + name: &'static str, + callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, +) -> (ProviderGuard, PluginRegistrationContext) { + let registry = InferenceProviderRegistry::default(); + let registration = registry + .register( + InferenceProviderDescriptor::new(name, PII_DETECTION_PROVIDER_CONTRACT).unwrap(), + Arc::new(callback), + ) + .unwrap(); + ( + ProviderGuard { + _registration: registration, + }, + PluginRegistrationContext::with_inference_providers(None, registry), + ) } fn backend( name: &'static str, callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, ) -> (ProviderGuard, CompiledLocalBackend) { - let registration_id = register_local_model_provider_tracked(name, Arc::new(callback)).unwrap(); + let (provider, ctx) = provider_context(name, callback); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some(name.into()), ..LocalBackendConfig::default() }, None, + &ctx, ) .unwrap(); - ( - ProviderGuard { - name, - registration_id, - }, - backend, - ) + (provider, backend) } fn alice_detector(request: Json, _timeout: Duration) -> PluginResult { @@ -124,15 +134,9 @@ fn malformed_or_overlapping_spans_fail_closed_for_the_batch() { #[test] fn provider_errors_fail_closed_without_changing_unselected_paths() { - let registration_id = register_local_model_provider_tracked( - "local-test-failure", - Arc::new(|_, _| Err(PluginError::RegistrationFailed("boom".into()))), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-failure", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-failure", |_, _| { + Err(PluginError::RegistrationFailed("boom".into())) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-failure".into()), @@ -141,6 +145,7 @@ fn provider_errors_fail_closed_without_changing_unselected_paths() { ..LocalBackendConfig::default() }, None, + &ctx, ) .unwrap(); @@ -183,19 +188,11 @@ fn batches_provider_requests_and_preserves_no_detection_values() { fn latency_budget_applies_to_the_entire_payload() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let registration_id = register_local_model_provider_tracked( - "local-test-total-deadline", - Arc::new(move |_, timeout| { - observed.fetch_add(1, Ordering::SeqCst); - std::thread::sleep(timeout + Duration::from_millis(5)); - Err(PluginError::RegistrationFailed("timed out".into())) - }), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-total-deadline", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-total-deadline", move |_, timeout| { + observed.fetch_add(1, Ordering::SeqCst); + std::thread::sleep(timeout + Duration::from_millis(5)); + Err(PluginError::RegistrationFailed("timed out".into())) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-total-deadline".into()), @@ -203,6 +200,7 @@ fn latency_budget_applies_to_the_entire_payload() { ..LocalBackendConfig::default() }, None, + &ctx, ) .unwrap(); let values = (0..(MAX_BATCH_ITEMS + 1)) @@ -317,15 +315,7 @@ fn non_utf8_boundary_detection_fails_closed() { #[test] fn local_policy_rejects_malformed_or_unbounded_values() { - let registration_id = register_local_model_provider_tracked( - "local-test-policy-bounds", - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-policy-bounds", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-policy-bounds", |request, _| Ok(request)); for (config, expected) in [ ( @@ -392,7 +382,7 @@ fn local_policy_rejects_malformed_or_unbounded_values() { "local.excluded_labels", ), ] { - let error = CompiledLocalBackend::new(config, None) + let error = CompiledLocalBackend::new(config, None, &ctx) .err() .expect("invalid local policy should fail"); assert!(error.to_string().contains(expected), "{error}"); @@ -407,13 +397,7 @@ fn local_policy_accepts_root_and_escaped_json_pointers() { #[test] fn target_path_patterns_match_one_segment_without_widening_exact_paths() { - let registration_id = - register_local_model_provider_tracked("local-test-path-patterns", Arc::new(alice_detector)) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-path-patterns", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-path-patterns", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-path-patterns".into()), @@ -422,6 +406,7 @@ fn target_path_patterns_match_one_segment_without_widening_exact_paths() { ..LocalBackendConfig::default() }, None, + &ctx, ) .unwrap(); @@ -447,15 +432,7 @@ fn target_path_patterns_match_one_segment_without_widening_exact_paths() { #[test] fn request_codec_classifies_only_normalized_content_patterns() { - let registration_id = register_local_model_provider_tracked( - "local-test-openai-request", - Arc::new(alice_detector), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-openai-request", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-openai-request", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-openai-request".into()), @@ -466,6 +443,7 @@ fn request_codec_classifies_only_normalized_content_patterns() { ..LocalBackendConfig::default() }, Some("openai_chat".into()), + &ctx, ) .unwrap(); let request = LlmRequest { @@ -518,15 +496,7 @@ fn request_codec_classifies_only_normalized_content_patterns() { #[test] fn response_codec_classifies_message_content_without_touching_identity_fields() { - let registration_id = register_local_model_provider_tracked( - "local-test-openai-response", - Arc::new(alice_detector), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-openai-response", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-openai-response", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-openai-response".into()), @@ -534,6 +504,7 @@ fn response_codec_classifies_message_content_without_touching_identity_fields() ..LocalBackendConfig::default() }, Some("openai_chat".into()), + &ctx, ) .unwrap(); let response = json!({ @@ -562,15 +533,7 @@ fn response_codec_classifies_message_content_without_touching_identity_fields() #[test] fn request_codec_failure_replaces_the_observable_body() { - let registration_id = register_local_model_provider_tracked( - "local-test-invalid-openai-request", - Arc::new(alice_detector), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-invalid-openai-request", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-invalid-openai-request", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-invalid-openai-request".into()), @@ -579,6 +542,7 @@ fn request_codec_failure_replaces_the_observable_body() { ..LocalBackendConfig::default() }, Some("openai_chat".into()), + &ctx, ) .unwrap(); let request = LlmRequest { @@ -600,15 +564,7 @@ fn request_codec_failure_replaces_the_observable_body() { #[test] fn request_codec_ambiguous_multi_message_edit_fails_closed() { - let registration_id = register_local_model_provider_tracked( - "local-test-ambiguous-openai-request", - Arc::new(alice_detector), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-ambiguous-openai-request", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-ambiguous-openai-request", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-ambiguous-openai-request".into()), @@ -617,6 +573,7 @@ fn request_codec_ambiguous_multi_message_edit_fails_closed() { ..LocalBackendConfig::default() }, Some("openai_chat".into()), + &ctx, ) .unwrap(); let request = LlmRequest { @@ -636,15 +593,7 @@ fn request_codec_ambiguous_multi_message_edit_fails_closed() { #[test] fn response_codec_failure_replaces_the_observable_payload() { - let registration_id = register_local_model_provider_tracked( - "local-test-invalid-openai-response", - Arc::new(alice_detector), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-invalid-openai-response", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-invalid-openai-response", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-invalid-openai-response".into()), @@ -653,6 +602,7 @@ fn response_codec_failure_replaces_the_observable_payload() { ..LocalBackendConfig::default() }, Some("openai_chat".into()), + &ctx, ) .unwrap(); let response = json!({ @@ -667,42 +617,34 @@ fn response_codec_failure_replaces_the_observable_payload() { #[test] fn host_policy_applies_score_threshold_and_label_exclusions() { - let registration_id = register_local_model_provider_tracked( - "local-test-detection-policy", - Arc::new(|_, _| { - Ok(json!({ - "version": 1, - "detections": [ - { - "text_id": 0, - "start_utf8": 0, - "end_utf8": 5, - "label": "LOW_SCORE", - "score": 0.49 - }, - { - "text_id": 0, - "start_utf8": 6, - "end_utf8": 11, - "label": "PRESERVE", - "score": 0.99 - }, - { - "text_id": 0, - "start_utf8": 12, - "end_utf8": 17, - "label": "REDACT", - "score": 0.99 - } - ] - })) - }), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-detection-policy", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-detection-policy", |_, _| { + Ok(json!({ + "version": 1, + "detections": [ + { + "text_id": 0, + "start_utf8": 0, + "end_utf8": 5, + "label": "LOW_SCORE", + "score": 0.49 + }, + { + "text_id": 0, + "start_utf8": 6, + "end_utf8": 11, + "label": "PRESERVE", + "score": 0.99 + }, + { + "text_id": 0, + "start_utf8": 12, + "end_utf8": 17, + "label": "REDACT", + "score": 0.99 + } + ] + })) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-detection-policy".into()), @@ -711,6 +653,7 @@ fn host_policy_applies_score_threshold_and_label_exclusions() { ..LocalBackendConfig::default() }, None, + &ctx, ) .unwrap(); @@ -722,26 +665,18 @@ fn host_policy_applies_score_threshold_and_label_exclusions() { #[test] fn validates_filtered_detections_before_applying_host_policy() { - let registration_id = register_local_model_provider_tracked( - "local-test-filtered-invalid-span", - Arc::new(|_, _| { - Ok(json!({ - "version": 1, - "detections": [{ - "text_id": 0, - "start_utf8": 0, - "end_utf8": 999, - "label": "LOW_SCORE", - "score": 0.1 - }] - })) - }), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-filtered-invalid-span", - registration_id, - }; + let (_provider, ctx) = provider_context("local-test-filtered-invalid-span", |_, _| { + Ok(json!({ + "version": 1, + "detections": [{ + "text_id": 0, + "start_utf8": 0, + "end_utf8": 999, + "label": "LOW_SCORE", + "score": 0.1 + }] + })) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-filtered-invalid-span".into()), @@ -749,6 +684,7 @@ fn validates_filtered_detections_before_applying_host_policy() { ..LocalBackendConfig::default() }, None, + &ctx, ) .unwrap(); @@ -760,34 +696,27 @@ fn validates_filtered_detections_before_applying_host_policy() { #[test] fn enforces_detection_limit_for_each_text() { - let registration_id = register_local_model_provider_tracked( - "local-test-per-text-detection-limit", - Arc::new(|_, _| { - let detections = (0..=MAX_DETECTIONS_PER_TEXT) - .map(|index| { - json!({ - "text_id": 0, - "start_utf8": index, - "end_utf8": index + 1, - "label": "NAME", - "score": 0.9 - }) + let (_provider, ctx) = provider_context("local-test-per-text-detection-limit", |_, _| { + let detections = (0..=MAX_DETECTIONS_PER_TEXT) + .map(|index| { + json!({ + "text_id": 0, + "start_utf8": index, + "end_utf8": index + 1, + "label": "NAME", + "score": 0.9 }) - .collect::>(); - Ok(json!({"version": 1, "detections": detections})) - }), - ) - .unwrap(); - let _provider = ProviderGuard { - name: "local-test-per-text-detection-limit", - registration_id, - }; + }) + .collect::>(); + Ok(json!({"version": 1, "detections": detections})) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-per-text-detection-limit".into()), ..LocalBackendConfig::default() }, None, + &ctx, ) .unwrap(); diff --git a/crates/pii-redaction/tests/worker_provider_tests.rs b/crates/pii-redaction/tests/worker_provider_tests.rs index 9d40752cb..3d6df4ee4 100644 --- a/crates/pii-redaction/tests/worker_provider_tests.rs +++ b/crates/pii-redaction/tests/worker_provider_tests.rs @@ -17,11 +17,9 @@ use nemo_relay::codec::traits::LlmResponseCodec; use nemo_relay::plugin::dynamic::{ DynamicPluginActivationSpec, DynamicPluginKind, PluginHostActivation, }; -use nemo_relay::plugin::{ - PluginComponentSpec, PluginConfig, clear_plugin_configuration, local_model_provider, -}; +use nemo_relay::plugin::{PluginComponentSpec, PluginConfig, clear_plugin_configuration}; use nemo_relay_pii_redaction::component::{ - PII_REDACTION_PLUGIN_KIND, register_pii_redaction_component, + PII_DETECTION_PROVIDER_CONTRACT, PII_REDACTION_PLUGIN_KIND, register_pii_redaction_component, }; use serde_json::{Map, json}; use tempfile::TempDir; @@ -77,7 +75,15 @@ async fn worker_provider_sanitizes_events_and_is_removed_after_host_clear() { .await .expect("worker and PII component should activate together"); assert!(!report.has_errors()); - assert!(local_model_provider("fixture_worker/fixture_local_model").is_ok()); + let inference_providers = activation.inference_providers(); + assert!( + inference_providers + .resolve( + "fixture_worker/fixture_local_model", + PII_DETECTION_PROVIDER_CONTRACT, + ) + .is_ok() + ); let events = Arc::new(Mutex::new(Vec::::new())); let captured = Arc::clone(&events); @@ -198,7 +204,12 @@ async fn worker_provider_sanitizes_events_and_is_removed_after_host_clear() { deregister_subscriber(subscriber_name).expect("test subscriber should deregister"); activation.clear().expect("plugin host should clear"); assert!( - local_model_provider("fixture_worker/fixture_local_model").is_err(), + inference_providers + .resolve( + "fixture_worker/fixture_local_model", + PII_DETECTION_PROVIDER_CONTRACT, + ) + .is_err(), "worker provider should not outlive its host activation" ); } @@ -249,6 +260,7 @@ async fn worker_exit_during_sanitization_fails_closed_and_removes_provider() { .await .expect("worker and PII component should activate together"); assert!(!report.has_errors()); + let inference_providers = activation.inference_providers(); let events = Arc::new(Mutex::new(Vec::::new())); let captured = Arc::clone(&events); @@ -285,7 +297,12 @@ async fn worker_exit_during_sanitization_fails_closed_and_removes_provider() { .to_string(); assert!(error.contains("shutdown"), "{error}"); assert!( - local_model_provider("fixture_worker/fixture_local_model").is_err(), + inference_providers + .resolve( + "fixture_worker/fixture_local_model", + PII_DETECTION_PROVIDER_CONTRACT, + ) + .is_err(), "failed worker provider should not survive host teardown" ); } diff --git a/crates/worker-proto/README.md b/crates/worker-proto/README.md index c3d6ca417..45c2aa3f2 100644 --- a/crates/worker-proto/README.md +++ b/crates/worker-proto/README.md @@ -43,9 +43,9 @@ tooling. clients, servers, services, and messages. - **JSON envelope helpers**: `json_envelope` and `decode_json_envelope` for serializing Relay DTOs into protocol payloads. -- **Language-neutral provider surface**: `LOCAL_MODEL_PROVIDER` carries - component-owned request and response JSON without coupling the host to the - worker implementation language. +- **Language-neutral provider surface**: `INFERENCE_PROVIDER` carries a + versioned contract identifier plus component-owned request and response JSON + without coupling the host to the worker implementation language. ## Installation diff --git a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto index 980cddee5..59d8fb3f7 100644 --- a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto +++ b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto @@ -52,7 +52,7 @@ enum RegistrationSurface { MARK_SANITIZE_GUARDRAIL = 30; SCOPE_SANITIZE_START_GUARDRAIL = 31; SCOPE_SANITIZE_END_GUARDRAIL = 32; - LOCAL_MODEL_PROVIDER = 40; + INFERENCE_PROVIDER = 40; } enum LlmCodecKind { @@ -143,7 +143,7 @@ message Registration { RegistrationSurface surface = 2; int32 priority = 3; bool break_chain = 4; - reserved 5; + string contract = 5; } message InvokeRequest { diff --git a/crates/worker-proto/tests/proto_tests.rs b/crates/worker-proto/tests/proto_tests.rs index c623726a3..6722c23ee 100644 --- a/crates/worker-proto/tests/proto_tests.rs +++ b/crates/worker-proto/tests/proto_tests.rs @@ -4,7 +4,8 @@ //! Tests for stable worker protocol helpers and enum values. use nemo_relay_worker_proto::v1::{ - HandshakeRequest, HealthRequest, InvokeRequest, JsonEnvelope, RegistrationSurface, ScopeType, + HandshakeRequest, HealthRequest, InvokeRequest, JsonEnvelope, Registration, + RegistrationSurface, ScopeType, }; use nemo_relay_worker_proto::{WORKER_PROTOCOL_GRPC_V1, decode_json_envelope, json_envelope}; use prost::Message; @@ -41,7 +42,13 @@ fn registration_surface_values_are_stable() { assert_eq!(RegistrationSurface::MarkSanitizeGuardrail as i32, 30); assert_eq!(RegistrationSurface::ScopeSanitizeStartGuardrail as i32, 31); assert_eq!(RegistrationSurface::ScopeSanitizeEndGuardrail as i32, 32); - assert_eq!(RegistrationSurface::LocalModelProvider as i32, 40); + assert_eq!(RegistrationSurface::InferenceProvider as i32, 40); + let encoded = Registration { + contract: "x".into(), + ..Default::default() + } + .encode_to_vec(); + assert_eq!(encoded, vec![42, 1, b'x']); } #[test] diff --git a/crates/worker/README.md b/crates/worker/README.md index 58098f007..16bb6fa9c 100644 --- a/crates/worker/README.md +++ b/crates/worker/README.md @@ -25,7 +25,7 @@ communicates with Relay through the versioned `grpc-v1` worker protocol. - **Isolate plugin code**: Run custom runtime behavior outside the Relay host process. - **Use typed registration APIs**: Implement `WorkerPlugin` and register - subscribers, guardrails, intercepts, or local-model providers with + subscribers, guardrails, intercepts, or inference providers with `PluginContext`. - **Call the host runtime**: Emit marks, manage scopes, and invoke middleware continuations through `PluginRuntime`. @@ -86,24 +86,28 @@ Relay supplies the socket, activation ID, and authentication token through the worker environment. Use `serve_plugin` for Relay-spawned workers; explicit server configuration is intended for tests and custom launchers. -## Local-Model Providers +## Inference Providers A worker can expose detector or inference functionality to a first-party host component without owning middleware policy: ```rust -ctx.register_local_model_provider("detector", |request| async move { - Ok(serde_json::json!({ - "version": 1, - "detections": detect(request)? - })) -}); +ctx.register_inference_provider( + "detector", + "acme.pii_detection.v1", + |request| async move { + Ok(serde_json::json!({ + "version": 1, + "detections": detect(request)? + })) + }, +); ``` The host publishes the provider as `/detector`. The consuming -component owns the request and response schema, deadline, field selection, -validation, and application of the result. The worker callback should perform -inference only. +component selects the exact contract and owns the request and response schema, +deadline, field selection, validation, and application of the result. The +worker callback should perform inference only. ## Concurrency and Cancellation diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 18f2acb95..900e0b5e9 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -270,7 +270,7 @@ type LlmRequestFn = Arc< type LlmExecutionFn = Arc BoxFutureResult + Send + Sync>; type LlmStreamExecutionFn = Arc BoxFutureResult + Send + Sync>; -type LocalModelProviderFn = Arc BoxFutureResult + Send + Sync>; +type InferenceProviderFn = Arc BoxFutureResult + Send + Sync>; #[derive(Default)] struct WorkerHandlers { @@ -290,7 +290,7 @@ struct WorkerHandlers { llm_requests: HashMap, llm_executions: HashMap, llm_stream_executions: HashMap, - local_model_providers: HashMap, + inference_providers: HashMap, } /// Registration context passed to [`WorkerPlugin::register`]. @@ -332,18 +332,18 @@ impl PluginContext { .insert(name.into(), Arc::new(callback)); } - /// Registers a named local-model request-response provider. + /// Registers a named inference provider for a versioned host contract. /// /// The provider receives and returns versioned JSON data owned by the /// consuming host component. It does not register middleware or decide /// which runtime fields are sanitized. - pub fn register_local_model_provider(&mut self, name: &str, callback: F) + pub fn register_inference_provider(&mut self, name: &str, contract: &str, callback: F) where F: Fn(Json) -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, { - self.push_registration(name, RegistrationSurface::LocalModelProvider, 0, false); - self.handlers.local_model_providers.insert( + self.push_contract_registration(name, RegistrationSurface::InferenceProvider, contract); + self.handlers.inference_providers.insert( name.into(), Arc::new(move |request| Box::pin(callback(request))), ); @@ -685,6 +685,22 @@ impl PluginContext { surface: surface as i32, priority, break_chain, + contract: String::new(), + }); + } + + fn push_contract_registration( + &mut self, + name: &str, + surface: RegistrationSurface, + contract: &str, + ) { + self.handlers.registrations.push(Registration { + local_name: name.into(), + surface: surface as i32, + priority: 0, + break_chain: false, + contract: contract.into(), }); } } @@ -1680,9 +1696,9 @@ impl WorkerService { | RegistrationSurface::LlmExecutionIntercept => { self.invoke_llm_response(request, &scope, surface).await } - RegistrationSurface::LocalModelProvider => { + RegistrationSurface::InferenceProvider => { let payload = provider_payload(request.payload)?; - let handler = self.local_model_provider(&request.registration_name)?; + let handler = self.inference_provider(&request.registration_name)?; let future = with_thread_scope(&scope, || handler(payload)); Ok(json_response(future.await?)) } @@ -2082,17 +2098,15 @@ impl WorkerService { }) } - fn local_model_provider(&self, name: &str) -> Result { + fn inference_provider(&self, name: &str) -> Result { self.handlers .lock() .map_err(|err| WorkerSdkError::Callback(format!("handler lock poisoned: {err}")))? - .local_model_providers + .inference_providers .get(name) .cloned() .ok_or_else(|| { - WorkerSdkError::InvalidInput(format!( - "local-model provider '{name}' not registered" - )) + WorkerSdkError::InvalidInput(format!("inference provider '{name}' not registered")) }) } } @@ -2219,7 +2233,7 @@ fn provider_payload( decode_json_envelope::(&value).map_err(Into::into) } _ => Err(WorkerSdkError::InvalidInput( - "expected local-model provider payload".into(), + "expected inference provider payload".into(), )), } } @@ -2547,7 +2561,7 @@ fn all_surfaces() -> Vec { RegistrationSurface::MarkSanitizeGuardrail, RegistrationSurface::ScopeSanitizeStartGuardrail, RegistrationSurface::ScopeSanitizeEndGuardrail, - RegistrationSurface::LocalModelProvider, + RegistrationSurface::InferenceProvider, ] } diff --git a/crates/worker/tests/worker_sdk_tests.rs b/crates/worker/tests/worker_sdk_tests.rs index 7214ff7e1..462523b8b 100644 --- a/crates/worker/tests/worker_sdk_tests.rs +++ b/crates/worker/tests/worker_sdk_tests.rs @@ -117,7 +117,7 @@ async fn worker_service_enforces_auth_and_reports_registrations() { assert!( handshake .supported_surfaces - .contains(&(RegistrationSurface::LocalModelProvider as i32)) + .contains(&(RegistrationSurface::InferenceProvider as i32)) ); let bad_health = client @@ -278,7 +278,7 @@ async fn worker_service_enforces_auth_and_reports_registrations() { } #[tokio::test(flavor = "multi_thread")] -async fn worker_service_invokes_local_model_provider() { +async fn worker_service_invokes_inference_provider() { let (handle, mut client) = spawn_worker( Arc::new(SurfacePlugin::default()), "http://127.0.0.1:9".into(), @@ -287,7 +287,8 @@ async fn worker_service_invokes_local_model_provider() { let registrations = register_plugin(&mut client).await; assert!(registrations.iter().any(|registration| { registration.local_name == "local-model" - && registration.surface == RegistrationSurface::LocalModelProvider as i32 + && registration.surface == RegistrationSurface::InferenceProvider as i32 + && registration.contract == "test.echo.v1" })); let response = invoke_json( @@ -1258,8 +1259,8 @@ async fn worker_service_reports_missing_handlers_and_malformed_payloads() { "llm execution", ), ( - provider_invoke("missing-local-model", json!({})), - "local-model provider", + provider_invoke("missing-inference-provider", json!({})), + "inference provider", ), ] { assert_worker_error( @@ -1964,7 +1965,7 @@ impl WorkerPlugin for SurfacePlugin { ctx.register_llm_stream_execution_intercept("llm-stream-open-error", 1, |_, _, _| async { Err(WorkerSdkError::Callback("stream open boom".into())) }); - ctx.register_local_model_provider("local-model", |request| async move { + ctx.register_inference_provider("local-model", "test.echo.v1", |request| async move { Ok(set_json_field(request, "provider", "local-model")) }); Ok(()) @@ -2564,7 +2565,7 @@ fn provider_invoke(registration_name: &str, value: Json) -> InvokeRequest { activation_id: ACTIVATION_ID.into(), invocation_id: "invoke-1".into(), registration_name: registration_name.into(), - surface: RegistrationSurface::LocalModelProvider as i32, + surface: RegistrationSurface::InferenceProvider as i32, continuation_id: String::new(), scope: Some(scope_context()), auth_token: AUTH_TOKEN.into(), diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx index e03f8bfff..85a703139 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx @@ -23,7 +23,7 @@ Workers implement the `PluginWorker` service: - `Handshake` and `Health` identify a ready worker. - `Validate` returns configuration diagnostics. - `Register` returns declarative subscriber, guardrail, intercept, and - local-model-provider registrations. + inference-provider registrations. - `Invoke` and `InvokeStream` run registered behavior. - `CancelInvocation` requests cancellation, and `Shutdown` requests process termination. @@ -69,17 +69,17 @@ return `WorkerError` without registrations. The supported surfaces are: `LLM_EXECUTION_INTERCEPT`, and `LLM_STREAM_EXECUTION_INTERCEPT` - `MARK_SANITIZE_GUARDRAIL`, `SCOPE_SANITIZE_START_GUARDRAIL`, and `SCOPE_SANITIZE_END_GUARDRAIL` -- `LOCAL_MODEL_PROVIDER` +- `INFERENCE_PROVIDER` `InvokeRequest` identifies the registration, surface, invocation, optional continuation, and scope context. Its payload is one of an event, tool invocation, LLM invocation, or component-owned provider request. `InvokeResponse` returns an empty result, JSON result, guardrail result, LLM request-intercept result, tool-execution result, or `WorkerError`. -`LOCAL_MODEL_PROVIDER` uses a JSON request and JSON result; the consuming -first-party component owns their versioned schema and publishes the provider -as `/`. `InvokeStream` emits JSON chunks or -`WorkerError` chunks. +`INFERENCE_PROVIDER` declares a versioned contract and uses a JSON request and +JSON result. The consuming first-party component selects that exact contract, +owns its schema, and resolves the provider as `/`. +`InvokeStream` emits JSON chunks or `WorkerError` chunks. Every LLM sanitizer invocation includes a directional context with tagged codec identity: `none`, `builtin(id)`, `runtime(id)`, or `opaque`. Worker SDKs expose diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx index 7ae0582e0..3c7451108 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx @@ -167,8 +167,8 @@ requests shutdown. ### Provide local inference -Use `register_local_model_provider` when a first-party Relay component owns the -policy and needs an isolated detector or inference implementation: +Use `register_inference_provider` when a first-party Relay component owns a +versioned contract and needs an isolated detector or inference implementation: ```python async def detect(request: Json) -> Json: @@ -178,12 +178,17 @@ async def detect(request: Json) -> Json: } -ctx.register_local_model_provider("detector", detect) +ctx.register_inference_provider( + "detector", + "acme.pii_detection.v1", + detect, +) ``` Relay publishes this registration as `/detector`. The consuming -component owns the JSON contract, field selection, deadline, response -validation, and application. The worker should perform inference only. +component selects the exact contract and owns its JSON schema, field selection, +deadline, response validation, and application. The worker should perform +inference only. ## Create the Manifest diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx index e2a18653b..cf687a433 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx @@ -70,21 +70,26 @@ directly for normal operation. ### Provide local inference -Use `register_local_model_provider` when a first-party Relay component owns the -policy and needs isolated inference: +Use `register_inference_provider` when a first-party Relay component owns a +versioned contract and needs isolated inference: ```rust -context.register_local_model_provider("detector", |request| async move { - Ok(serde_json::json!({ - "version": 1, - "detections": detect(request)? - })) -}); +context.register_inference_provider( + "detector", + "acme.pii_detection.v1", + |request| async move { + Ok(serde_json::json!({ + "version": 1, + "detections": detect(request)? + })) + }, +); ``` Relay publishes this registration as `/detector`. The consuming -component owns the JSON contract, field selection, deadline, response -validation, and application. The worker should perform inference only. +component selects the exact contract and owns its JSON schema, field selection, +deadline, response validation, and application. The worker should perform +inference only. ## Package the Worker diff --git a/docs/configure-plugins/pii-redaction/configuration.mdx b/docs/configure-plugins/pii-redaction/configuration.mdx index 51979e6e0..95934911c 100644 --- a/docs/configure-plugins/pii-redaction/configuration.mdx +++ b/docs/configure-plugins/pii-redaction/configuration.mdx @@ -323,8 +323,10 @@ fields, batches text, enforces deadlines, validates detections, applies confidence and label policy, replaces accepted spans, and fails closed. The provider name is `/`. For example, a worker -with plugin ID `acme.pii_worker` that registers `detector` is selected as -`acme.pii_worker/detector`. +with plugin ID `acme.pii_worker` that registers `detector` for +`nemo.relay.pii_detection.v1` is selected as `acme.pii_worker/detector`. Relay +rejects providers that declare another contract before installing the +sanitizer. ```toml [[components]] diff --git a/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py b/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py index c8b13408f..e9d9060bb 100644 --- a/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py +++ b/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py @@ -71,7 +71,11 @@ async def echo_local_model(request: Json) -> Json: } ctx.register_tool_request_intercept("tag_tool_request", tag_tool_request) - ctx.register_local_model_provider("echo", echo_local_model) + ctx.register_inference_provider( + "echo", + "examples.python_grpc_worker.echo.v1", + echo_local_model, + ) def _tag_json(value: Json, tag: str) -> Json: diff --git a/examples/python-grpc-worker-plugin/relay-plugin.toml b/examples/python-grpc-worker-plugin/relay-plugin.toml index adc56b525..b55df6679 100644 --- a/examples/python-grpc-worker-plugin/relay-plugin.toml +++ b/examples/python-grpc-worker-plugin/relay-plugin.toml @@ -22,7 +22,7 @@ manifest_root = "." artifact = "nemo_relay_python_grpc_worker_example/worker.py" [integrity] -sha256 = "sha256:64b20982d6309816947a8de5f9893557fb145c47b4a6fbffc48d506e7f5c695e" +sha256 = "sha256:f7313049118e931adfd7d1c9ab18dc5aefedf35dd01d5efea4dc12306ee08358" [load] runtime = "python" diff --git a/justfile b/justfile index 1819fad5c..937dec95d 100644 --- a/justfile +++ b/justfile @@ -971,7 +971,8 @@ check-python-worker-proto: } assert pb.SUBSCRIBER == 1 assert pb.LLM_STREAM_EXECUTION_INTERCEPT == 25 - assert pb.LOCAL_MODEL_PROVIDER == 40 + assert pb.INFERENCE_PROVIDER == 40 + assert pb.Registration.DESCRIPTOR.fields_by_name["contract"].number == 5 assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["provider"].number == 13 PY diff --git a/python/plugin/README.md b/python/plugin/README.md index 1fff3c2d7..3eac28c7a 100644 --- a/python/plugin/README.md +++ b/python/plugin/README.md @@ -26,7 +26,7 @@ protocol. - **Isolate plugin dependencies**: Run custom policy, middleware, or exporter code outside the Relay host process. - **Use the shared runtime contract**: Register subscribers, guardrails, and - intercepts or local-model providers through `WorkerPlugin` and + intercepts or inference providers through `WorkerPlugin` and `PluginContext`. - **Call back into Relay safely**: Emit marks, create scopes, and continue managed execution through the host runtime handle. @@ -105,10 +105,10 @@ worker process. For a complete manifest and runnable plugin, see the [Python gRPC worker plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/python-grpc-worker-plugin/README.md). -## Local-Model Providers +## Inference Providers -Use `register_local_model_provider` when a first-party Relay component owns the -policy and needs isolated model inference: +Use `register_inference_provider` when a first-party Relay component owns a +versioned request-response contract and needs isolated model inference: ```python async def detect(request: Json) -> Json: @@ -118,13 +118,17 @@ async def detect(request: Json) -> Json: } -ctx.register_local_model_provider("detector", detect) +ctx.register_inference_provider( + "detector", + "acme.pii_detection.v1", + detect, +) ``` Relay publishes this provider as `/detector`. The callback may be synchronous or asynchronous and should perform inference only. The consuming -host component owns the payload schema, deadline, field traversal, output -validation, and result application. +host component selects the exact contract and owns the payload schema, +deadline, field traversal, output validation, and result application. ## Request Intercepts diff --git a/python/plugin/src/nemo_relay_plugin/__init__.py b/python/plugin/src/nemo_relay_plugin/__init__.py index 9ea5a4b2b..a3bb23893 100644 --- a/python/plugin/src/nemo_relay_plugin/__init__.py +++ b/python/plugin/src/nemo_relay_plugin/__init__.py @@ -36,7 +36,7 @@ LlmOptimizationTokens: Explicit token evidence by category. LlmOptimizationTokenImpact: Baseline, effective, and saved token evidence. LlmRequestInterceptOutcome: Canonical LLM request-intercept result. - LocalModelProviderCallback: Local-model request-response provider callback. + InferenceProviderCallback: Versioned inference-provider callback. ToolExecutionInterceptOutcome: Canonical tool execution-intercept result. DiagnosticLevel: Severity of a configuration diagnostic. ConfigDiagnostic: Structured configuration warning or error. @@ -76,6 +76,7 @@ Event, EventSanitizeCallback, EventSanitizeFields, + InferenceProviderCallback, Json, LlmCodecIdentity, LlmConditionalCallback, @@ -97,7 +98,6 @@ LlmSanitizeResponseContext, LlmStreamExecutionCallback, LlmStreamNext, - LocalModelProviderCallback, PendingMarkSpec, PluginContext, PluginRuntime, @@ -144,7 +144,7 @@ "LlmSanitizeResponseCallback", "LlmStreamNext", "LlmStreamExecutionCallback", - "LocalModelProviderCallback", + "InferenceProviderCallback", "PluginContext", "PluginRuntime", "PendingMarkSpec", diff --git a/python/plugin/src/nemo_relay_plugin/_api.py b/python/plugin/src/nemo_relay_plugin/_api.py index 5ad1d044d..d14d390ff 100644 --- a/python/plugin/src/nemo_relay_plugin/_api.py +++ b/python/plugin/src/nemo_relay_plugin/_api.py @@ -845,7 +845,7 @@ def register(self, ctx: PluginContext, config: Json) -> None | Awaitable[None]: [str, LlmRequest, "LlmStreamNext"], Iterable[Json] | AsyncIterator[Json] | Awaitable[Iterable[Json] | AsyncIterator[Json]], ] -LocalModelProviderCallback: TypeAlias = Callable[[Json], Json | Awaitable[Json]] +InferenceProviderCallback: TypeAlias = Callable[[Json], Json | Awaitable[Json]] @dataclass(slots=True) @@ -866,7 +866,7 @@ class _Handlers: llm_requests: dict[str, LlmRequestCallback] llm_executions: dict[str, LlmExecutionCallback] llm_stream_executions: dict[str, LlmStreamExecutionCallback] - local_model_providers: dict[str, LocalModelProviderCallback] + inference_providers: dict[str, InferenceProviderCallback] @classmethod def empty(cls) -> _Handlers: @@ -887,7 +887,7 @@ def empty(cls) -> _Handlers: llm_requests={}, llm_executions={}, llm_stream_executions={}, - local_model_providers={}, + inference_providers={}, ) @@ -956,15 +956,17 @@ def register_subscriber(self, name: str, callback: SubscriberCallback) -> None: self._push_registration(name, pb.SUBSCRIBER, 0, False) self._handlers.subscribers[name] = callback - def register_local_model_provider( + def register_inference_provider( self, name: str, - callback: LocalModelProviderCallback, + contract: str, + callback: InferenceProviderCallback, ) -> None: - """Register a named local-model request-response provider. + """Register a named inference provider for a versioned host contract. Args: name: Stable provider name selected by a consuming host component. + contract: Versioned request-response contract implemented by the provider. callback: Function receiving and returning component-owned JSON. The callback can return a value directly or through an awaitable. @@ -973,8 +975,8 @@ def register_local_model_provider( Providers perform model inference only. The consuming host component owns field selection, policy, and output application. """ - self._push_registration(name, pb.LOCAL_MODEL_PROVIDER, 0, False) - self._handlers.local_model_providers[name] = callback + self._push_registration(name, pb.INFERENCE_PROVIDER, 0, False, contract=contract) + self._handlers.inference_providers[name] = callback def _register_event_sanitizer( self, @@ -1283,7 +1285,15 @@ def register_llm_stream_execution_intercept( self._push_registration(name, pb.LLM_STREAM_EXECUTION_INTERCEPT, priority, False) self._handlers.llm_stream_executions[name] = callback - def _push_registration(self, name: str, surface: int, priority: int, break_chain: bool) -> None: + def _push_registration( + self, + name: str, + surface: int, + priority: int, + break_chain: bool, + *, + contract: str = "", + ) -> None: if any( registration.local_name == name and registration.surface == surface for registration in self._handlers.registrations @@ -1295,6 +1305,7 @@ def _push_registration(self, name: str, surface: int, priority: int, break_chain surface=surface, priority=priority, break_chain=break_chain, + contract=contract, ) ) @@ -2083,15 +2094,15 @@ async def _invoke_result(self, request: Any) -> Any: ), ) ) - if request.surface == pb.LOCAL_MODEL_PROVIDER: + if request.surface == pb.INFERENCE_PROVIDER: result = await _maybe_await( self._handler( - self._handlers.local_model_providers, + self._handlers.inference_providers, request.registration_name, )( _decode_required_envelope( request.provider, - "local-model provider request", + "inference provider request", ) ) ) @@ -2231,7 +2242,7 @@ def _all_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, - pb.LOCAL_MODEL_PROVIDER, + pb.INFERENCE_PROVIDER, ] diff --git a/python/tests/plugin/test_public_api_docstrings.py b/python/tests/plugin/test_public_api_docstrings.py index aa78a349a..4ab3074c8 100644 --- a/python/tests/plugin/test_public_api_docstrings.py +++ b/python/tests/plugin/test_public_api_docstrings.py @@ -35,7 +35,7 @@ "LlmRequestCallback", "LlmExecutionCallback", "LlmStreamExecutionCallback", - "LocalModelProviderCallback", + "InferenceProviderCallback", } diff --git a/python/tests/plugin/test_worker_sdk.py b/python/tests/plugin/test_worker_sdk.py index fcb2c146d..20062585d 100644 --- a/python/tests/plugin/test_worker_sdk.py +++ b/python/tests/plugin/test_worker_sdk.py @@ -363,7 +363,7 @@ async def llm_stream_execution(name: str, request: Json, next_call: Any) -> Asyn async for chunk in stream: yield _tag(chunk, "llm_stream_execution") - async def local_model_provider(request: Json) -> Json: + async def inference_provider(request: Json) -> Json: return _tag(request, "local_model") ctx.register_subscriber("subscriber", subscriber) @@ -381,7 +381,11 @@ async def local_model_provider(request: Json) -> Json: ctx.register_llm_request_intercept("llm_request", llm_request, priority=9, break_chain=True) ctx.register_llm_execution_intercept("llm_execution", llm_execution, priority=10) ctx.register_llm_stream_execution_intercept("llm_stream_execution", llm_stream_execution, priority=11) - ctx.register_local_model_provider("local_model", local_model_provider) + ctx.register_inference_provider( + "local_model", + "test.echo.v1", + inference_provider, + ) @pytest.fixture(name="host_stub") @@ -415,7 +419,8 @@ def test_generated_proto_matches_worker_contract(): assert pb.MARK_SANITIZE_GUARDRAIL == 30 assert pb.SCOPE_SANITIZE_START_GUARDRAIL == 31 assert pb.SCOPE_SANITIZE_END_GUARDRAIL == 32 - assert pb.LOCAL_MODEL_PROVIDER == 40 + assert pb.INFERENCE_PROVIDER == 40 + assert pb.Registration.DESCRIPTOR.fields_by_name["contract"].number == 5 assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["provider"].number == 13 assert pb.CUSTOM == 10 @@ -450,26 +455,32 @@ async def test_health_handshake_validate_register_and_all_surfaces(service: _Wor register = await _register(service) registrations = [ - (registration.local_name, registration.surface, registration.priority, registration.break_chain) + ( + registration.local_name, + registration.surface, + registration.priority, + registration.break_chain, + registration.contract, + ) for registration in register.registrations ] assert registrations == [ - ("subscriber", pb.SUBSCRIBER, 0, False), - ("event_sanitize", pb.MARK_SANITIZE_GUARDRAIL, 1, False), - ("event_sanitize", pb.SCOPE_SANITIZE_START_GUARDRAIL, 2, False), - ("scope_end_sanitize", pb.SCOPE_SANITIZE_END_GUARDRAIL, 3, False), - ("tool_sanitize", pb.TOOL_SANITIZE_REQUEST_GUARDRAIL, 1, False), - ("tool_sanitize", pb.TOOL_SANITIZE_RESPONSE_GUARDRAIL, 2, False), - ("tool_conditional", pb.TOOL_CONDITIONAL_EXECUTION_GUARDRAIL, 3, False), - ("tool_request", pb.TOOL_REQUEST_INTERCEPT, 4, True), - ("tool_execution", pb.TOOL_EXECUTION_INTERCEPT, 5, False), - ("llm_sanitize_request", pb.LLM_SANITIZE_REQUEST_GUARDRAIL, 6, False), - ("llm_sanitize_response", pb.LLM_SANITIZE_RESPONSE_GUARDRAIL, 7, False), - ("llm_conditional", pb.LLM_CONDITIONAL_EXECUTION_GUARDRAIL, 8, False), - ("llm_request", pb.LLM_REQUEST_INTERCEPT, 9, True), - ("llm_execution", pb.LLM_EXECUTION_INTERCEPT, 10, False), - ("llm_stream_execution", pb.LLM_STREAM_EXECUTION_INTERCEPT, 11, False), - ("local_model", pb.LOCAL_MODEL_PROVIDER, 0, False), + ("subscriber", pb.SUBSCRIBER, 0, False, ""), + ("event_sanitize", pb.MARK_SANITIZE_GUARDRAIL, 1, False, ""), + ("event_sanitize", pb.SCOPE_SANITIZE_START_GUARDRAIL, 2, False, ""), + ("scope_end_sanitize", pb.SCOPE_SANITIZE_END_GUARDRAIL, 3, False, ""), + ("tool_sanitize", pb.TOOL_SANITIZE_REQUEST_GUARDRAIL, 1, False, ""), + ("tool_sanitize", pb.TOOL_SANITIZE_RESPONSE_GUARDRAIL, 2, False, ""), + ("tool_conditional", pb.TOOL_CONDITIONAL_EXECUTION_GUARDRAIL, 3, False, ""), + ("tool_request", pb.TOOL_REQUEST_INTERCEPT, 4, True, ""), + ("tool_execution", pb.TOOL_EXECUTION_INTERCEPT, 5, False, ""), + ("llm_sanitize_request", pb.LLM_SANITIZE_REQUEST_GUARDRAIL, 6, False, ""), + ("llm_sanitize_response", pb.LLM_SANITIZE_RESPONSE_GUARDRAIL, 7, False, ""), + ("llm_conditional", pb.LLM_CONDITIONAL_EXECUTION_GUARDRAIL, 8, False, ""), + ("llm_request", pb.LLM_REQUEST_INTERCEPT, 9, True, ""), + ("llm_execution", pb.LLM_EXECUTION_INTERCEPT, 10, False, ""), + ("llm_stream_execution", pb.LLM_STREAM_EXECUTION_INTERCEPT, 11, False, ""), + ("local_model", pb.INFERENCE_PROVIDER, 0, False, "test.echo.v1"), ] @@ -2765,7 +2776,7 @@ def _tool_request(registration_name: str, surface: int, value: Json) -> Any: def _provider_request(registration_name: str, value: Json) -> Any: return _invoke_request( registration_name, - pb.LOCAL_MODEL_PROVIDER, + pb.INFERENCE_PROVIDER, continuation_id="", provider=_json_envelope(JSON_SCHEMA, value), ) @@ -2859,5 +2870,5 @@ def _all_expected_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, - pb.LOCAL_MODEL_PROVIDER, + pb.INFERENCE_PROVIDER, ] From 0c9ac1d93f6efe4a506ee573725fafee51d99913 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sun, 26 Jul 2026 14:49:26 -0700 Subject: [PATCH 05/83] refactor(pii): harden local model provider path Signed-off-by: Alex Fournier --- crates/node/tests/pii_redaction_tests.mjs | 26 ++ crates/pii-redaction/README.md | 18 +- .../pii-redaction/providers/rampart/README.md | 27 +- .../rampart/nemo_relay_pii_rampart/worker.py | 13 +- .../providers/rampart/relay-plugin.toml | 2 +- .../providers/rampart/tests/test_worker.py | 33 ++ crates/pii-redaction/src/builtin.rs | 7 + crates/pii-redaction/src/component.rs | 2 +- crates/pii-redaction/src/local.rs | 287 ++++++++++++++---- .../tests/unit/component_tests.rs | 42 +++ .../pii-redaction/tests/unit/local_tests.rs | 263 +++++++++++++++- .../configure-plugins/pii-redaction/about.mdx | 4 +- .../pii-redaction/configuration.mdx | 13 +- .../pii_redaction/pii_redaction_test.go | 18 +- python/tests/test_pii_redaction_plugin.py | 4 + 15 files changed, 656 insertions(+), 103 deletions(-) diff --git a/crates/node/tests/pii_redaction_tests.mjs b/crates/node/tests/pii_redaction_tests.mjs index ad529e292..e696e4880 100644 --- a/crates/node/tests/pii_redaction_tests.mjs +++ b/crates/node/tests/pii_redaction_tests.mjs @@ -23,6 +23,32 @@ describe('pii_redaction plugin helpers', () => { }); assert.deepEqual(piiRedaction.builtinConfig(), { action: 'remove' }); assert.deepEqual(piiRedaction.localModelConfig(), {}); + assert.deepEqual( + piiRedaction.localModelConfig({ + backend: 'acme.pii/detector', + model_id: 'pii-model-v1', + detector_profile: 'default', + target_paths: ['/message'], + target_path_patterns: ['/messages/*/content'], + min_score: 0.6, + excluded_labels: ['CITY'], + replacement: '[PRIVATE]', + allow_network: false, + max_latency_ms: 250, + }), + { + backend: 'acme.pii/detector', + model_id: 'pii-model-v1', + detector_profile: 'default', + target_paths: ['/message'], + target_path_patterns: ['/messages/*/content'], + min_score: 0.6, + excluded_labels: ['CITY'], + replacement: '[PRIVATE]', + allow_network: false, + max_latency_ms: 250, + }, + ); assert.deepEqual(piiRedaction.profileConfig(), { enabled: true, mode: 'builtin', diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index a536d8649..10f3d4cf6 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -250,7 +250,7 @@ priority = 90 [components.config.profiles.local] backend = "nemo_relay.pii_rampart/detector" min_score = 0.4 -max_latency_ms = 1500 +max_latency_ms = 5000 target_path_patterns = [ "/messages/*/content", "/messages/*/content/*/text", @@ -279,14 +279,14 @@ redaction policy. Provider failures, timeouts, malformed responses, invalid UTF-8 boundaries, overlapping spans, and input-limit violations fail closed for the affected batch. If a configured codec cannot decode or safely re-encode an LLM payload, -Relay replaces the entire emitted request or response body; it does not retry -normalized selectors against the raw provider shape. `allow_network = true` is -rejected; this lane is for same-machine inference. This setting is a -configuration invariant, not a network sandbox: Relay's worker launcher does -not currently prevent a worker process from opening sockets. Only install -providers whose packaging and runtime behavior satisfy that policy. The -default deadline is 250 ms for the complete selected payload, including every -provider batch. Configuration above 60 seconds is rejected. +Relay omits that request or response payload from the emitted event; it does not +retry normalized selectors against the raw provider shape. +`allow_network = true` is rejected; this lane is for same-machine inference. +This setting is a configuration invariant, not a network sandbox: Relay's +worker launcher does not currently prevent a worker process from opening +sockets. Only install providers whose packaging and runtime behavior satisfy +that policy. The default deadline is 250 ms for the complete selected payload, +including every provider batch. Configuration above 60 seconds is rejected. ### Provider Contract diff --git a/crates/pii-redaction/providers/rampart/README.md b/crates/pii-redaction/providers/rampart/README.md index ec8b811c1..ae1458a94 100644 --- a/crates/pii-redaction/providers/rampart/README.md +++ b/crates/pii-redaction/providers/rampart/README.md @@ -77,15 +77,34 @@ kind = "pii_redaction" enabled = true [components.config] -mode = "local_model" codec = "openai_chat" -[components.config.local] +[[components.config.profiles]] +mode = "builtin" +priority = 70 + +[components.config.profiles.builtin] +action = "redact" +detector = "email" + +[[components.config.profiles]] +mode = "builtin" +priority = 80 + +[components.config.profiles.builtin] +action = "redact" +detector = "credit_card" + +[[components.config.profiles]] +mode = "local_model" +priority = 90 + +[components.config.profiles.local] backend = "nemo_relay.pii_rampart/detector" model_id = "nationaldesignstudio/rampart" detector_profile = "default" allow_network = false -max_latency_ms = 1500 +max_latency_ms = 5000 min_score = 0.4 replacement = "[REDACTED]" target_path_patterns = [ @@ -122,7 +141,7 @@ optional `excluded_labels` policy after validating the provider response. - CPU inference is serialized per worker process. Host deadlines cancel the RPC, while already-running native inference is allowed to finish before its admission slot is released. -- Use a `max_latency_ms` of at least 1500 when the selected payload can approach +- Use a `max_latency_ms` of at least 5000 when the selected payload can approach the 64 KiB provider-request limit. Smaller content-only payloads normally complete much faster. Benchmark representative inputs on deployment hardware before lowering the deadline. diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py index bd62e814d..d04533e99 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py @@ -66,15 +66,26 @@ def register(self, ctx: PluginContext, config: Json) -> None: settings = RampartSettings.from_config(config) detector = RampartDetector.load(settings) admission = _Admission(settings.max_pending_requests) + inference_slot = asyncio.Lock() async def detect(request: Json) -> Json: admission.acquire() - work = asyncio.create_task(asyncio.to_thread(detector.detect_request, request)) + native_started = False + + async def run_native() -> Json: + nonlocal native_started + async with inference_slot: + native_started = True + return await asyncio.to_thread(detector.detect_request, request) + + work = asyncio.create_task(run_native()) release_on_completion = False try: return await asyncio.shield(work) except asyncio.CancelledError: release_on_completion = True + if not native_started: + work.cancel() def release_after_work(_task: asyncio.Task[Json]) -> None: try: diff --git a/crates/pii-redaction/providers/rampart/relay-plugin.toml b/crates/pii-redaction/providers/rampart/relay-plugin.toml index 67d382a1e..a83fdaf18 100644 --- a/crates/pii-redaction/providers/rampart/relay-plugin.toml +++ b/crates/pii-redaction/providers/rampart/relay-plugin.toml @@ -25,7 +25,7 @@ manifest_root = "." artifact = "nemo_relay_pii_rampart/worker.py" [integrity] -sha256 = "sha256:9b4bc53676e6c74c12d48212d8ed7e9438b5763001299224724e9bee56a0fda5" +sha256 = "sha256:e0ab2677112b8687d0bee32b257373a4057878f437ec8b66a233cb7b02533338" [load] runtime = "python" diff --git a/crates/pii-redaction/providers/rampart/tests/test_worker.py b/crates/pii-redaction/providers/rampart/tests/test_worker.py index 7c590f402..e7e7fc0b7 100644 --- a/crates/pii-redaction/providers/rampart/tests/test_worker.py +++ b/crates/pii-redaction/providers/rampart/tests/test_worker.py @@ -122,6 +122,39 @@ async def exercise() -> None: asyncio.run(exercise()) +def test_cancelled_queued_callback_does_not_run_native_inference(monkeypatch: pytest.MonkeyPatch) -> None: + started = threading.Event() + release = threading.Event() + requests: list[int] = [] + + class RecordingDetector: + def detect_request(self, request: Any) -> dict[str, Any]: + requests.append(request["texts"][0]["id"]) + started.set() + release.wait(timeout=5) + return {"version": 1, "detections": []} + + monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: RecordingDetector()) + context = FakeContext() + RampartWorker().register(cast(PluginContext, context), {"max_pending_requests": 2}) + + async def exercise() -> None: + first = asyncio.create_task(context.callback({"version": 1, "texts": [{"id": 0, "text": "one"}]})) + assert await asyncio.to_thread(started.wait, 1) + second = asyncio.create_task(context.callback({"version": 1, "texts": [{"id": 1, "text": "two"}]})) + await asyncio.sleep(0) + second.cancel() + with pytest.raises(asyncio.CancelledError): + await second + + release.set() + await first + await context.callback({"version": 1, "texts": [{"id": 2, "text": "three"}]}) + + asyncio.run(exercise()) + assert requests == [0, 2] + + def test_detector_failure_releases_admission(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: FailingDetector()) context = FakeContext() diff --git a/crates/pii-redaction/src/builtin.rs b/crates/pii-redaction/src/builtin.rs index 1fac4b952..292da0430 100644 --- a/crates/pii-redaction/src/builtin.rs +++ b/crates/pii-redaction/src/builtin.rs @@ -534,6 +534,10 @@ pub(super) fn llm_sanitize_request_callback( request.content = backend.sanitize_json_preorder_dfs(request.content); return Some(request); } + if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { + request.content = backend.sanitize_json_preorder_dfs(request.content); + return Some(request); + } let resolved = context.resolve_codec(); let fallback = if resolved.is_none() { backend @@ -568,6 +572,9 @@ pub(super) fn llm_sanitize_response_callback( if backend.target_paths.is_empty() { return Some(backend.sanitize_json_preorder_dfs(payload)); } + if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { + return Some(backend.sanitize_json_preorder_dfs(payload)); + } if matches!(context.codec(), LlmCodecIdentity::None) && !backend.uses_compatible_legacy_response_codec(&payload) { diff --git a/crates/pii-redaction/src/component.rs b/crates/pii-redaction/src/component.rs index 3165e4a25..21b90a223 100644 --- a/crates/pii-redaction/src/component.rs +++ b/crates/pii-redaction/src/component.rs @@ -278,7 +278,7 @@ pub struct LocalBackendConfig { /// Whether the provider may use network calls. #[serde(default, skip_serializing_if = "Option::is_none")] pub allow_network: Option, - /// Per-batch provider deadline in milliseconds. + /// Total provider deadline for one selected payload in milliseconds. #[serde(default, skip_serializing_if = "Option::is_none")] pub max_latency_ms: Option, } diff --git a/crates/pii-redaction/src/local.rs b/crates/pii-redaction/src/local.rs index 5ba225764..17dd3c019 100644 --- a/crates/pii-redaction/src/local.rs +++ b/crates/pii-redaction/src/local.rs @@ -5,13 +5,15 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, Instant}; -use nemo_relay::api::event::{CategoryProfile, Event}; +use nemo_relay::api::event::Event; use nemo_relay::api::llm::LlmRequest; use nemo_relay::api::runtime::{ - EventSanitizeFn, LlmSanitizeRequestFn, LlmSanitizeResponseFn, ToolSanitizeFn, + BuiltinLlmCodec, EventSanitizeFn, LlmCodecIdentity, LlmSanitizeRequestFn, + LlmSanitizeResponseFn, ToolSanitizeFn, }; use nemo_relay::codec::resolve::{ - ProviderSurface, request_codec as build_request_codec, response_codec as build_response_codec, + ProviderSurface, detect_response_surface, request_codec as build_request_codec, + response_codec as build_response_codec, }; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_relay::plugin::{ @@ -19,7 +21,7 @@ use nemo_relay::plugin::{ }; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use serde_json::Value as Json; +use serde_json::{Map, Value as Json}; use super::component::{ DEFAULT_LOCAL_MODEL_LATENCY_MS, DEFAULT_LOCAL_MODEL_MIN_SCORE, LocalBackendConfig, @@ -45,15 +47,13 @@ struct CompiledLocalBackend { provider: InferenceProvider, model_id: Option, detector_profile: Option, - target_paths: Arc>, + target_paths: Arc>>, target_path_patterns: Arc>, min_score: f64, excluded_labels: Arc>, replacement: Arc, timeout: Duration, - request_codec: Option>, - response_codec: Option>, - codec_name: Option, + legacy_surface: Option, } #[derive(Clone)] @@ -63,10 +63,9 @@ struct JsonPointerPattern { impl JsonPointerPattern { fn compile(pattern: String) -> Self { - let segments = pattern.strip_prefix('/').map_or_else(Vec::new, |path| { - path.split('/').map(str::to_string).collect() - }); - Self { segments } + Self { + segments: compile_json_pointer(pattern), + } } fn matches(&self, path: &[String]) -> bool { @@ -117,6 +116,12 @@ struct SelectedText { eligible: bool, } +enum EventField { + Data, + CategoryProfile, + Metadata, +} + impl CompiledLocalBackend { fn new( config: LocalBackendConfig, @@ -159,7 +164,13 @@ impl CompiledLocalBackend { detector_profile: config .detector_profile .map(|value| value.trim().to_string()), - target_paths: Arc::new(config.target_paths.into_iter().collect()), + target_paths: Arc::new( + config + .target_paths + .into_iter() + .map(compile_json_pointer) + .collect(), + ), target_path_patterns: Arc::new( config .target_path_patterns @@ -177,27 +188,44 @@ impl CompiledLocalBackend { ), replacement: Arc::new(replacement), timeout: Duration::from_millis(max_latency_ms), - request_codec: surface.map(build_request_codec), - response_codec: surface.map(build_response_codec), - codec_name: surface.map(BuiltinCodecName::from_provider_surface), + legacy_surface: surface, }) } - fn sanitize_json(&self, mut value: Json) -> Json { + fn sanitize_json(&self, value: Json) -> Json { + self.sanitize_json_values(vec![value]) + .pop() + .expect("single-value sanitization returns one value") + } + + fn sanitize_json_values(&self, values: Vec) -> Vec { + self.sanitize_json_roots( + values + .into_iter() + .map(|value| (Vec::new(), value)) + .collect(), + ) + } + + fn sanitize_json_roots(&self, mut roots: Vec<(Vec, Json)>) -> Vec { let mut texts = Vec::new(); let mut total_bytes = 0; let mut within_budget = true; - self.collect_strings( - &value, - &mut Vec::new(), - &mut texts, - &mut total_bytes, - &mut within_budget, - ); + for (path, value) in &mut roots { + self.collect_strings( + value, + path, + &mut texts, + &mut total_bytes, + &mut within_budget, + ); + } let sanitized = self.sanitize_texts(texts); let mut index = 0; - self.replace_strings(&mut value, &mut Vec::new(), &sanitized, &mut index); - value + for (path, value) in &mut roots { + self.replace_strings(value, path, &sanitized, &mut index); + } + roots.into_iter().map(|(_, value)| value).collect() } fn collect_strings( @@ -289,9 +317,7 @@ impl CompiledLocalBackend { fn matches_path(&self, path: &[String]) -> bool { (self.target_paths.is_empty() && self.target_path_patterns.is_empty()) - || self - .target_paths - .contains(&super::builtin::render_json_pointer_path(path)) + || self.target_paths.contains(path) || self .target_path_patterns .iter() @@ -464,34 +490,103 @@ impl CompiledLocalBackend { Ok(replacements) } - fn sanitize_request_with_codec(&self, request: &LlmRequest) -> Option { - let codec = self.request_codec.as_ref()?; + fn sanitize_request_with_codec( + &self, + codec: &dyn LlmCodec, + request: &LlmRequest, + ) -> Option { let annotated = codec.decode(request).ok()?; - let sanitized = sanitize_serializable(self, annotated).ok()?; - codec.encode(&sanitized, request).ok() + let annotated = serde_json::to_value(annotated).ok()?; + let (headers, annotated) = + self.sanitize_request_parts(request.headers.clone(), annotated)?; + let annotated = serde_json::from_value(annotated).ok()?; + let mut encoded = codec.encode(&annotated, request).ok()?; + encoded.headers = headers; + Some(encoded) + } + + fn sanitize_raw_request(&self, mut request: LlmRequest) -> Option { + let headers = std::mem::take(&mut request.headers); + let content = std::mem::take(&mut request.content); + let (headers, content) = self.sanitize_request_parts(headers, content)?; + request.headers = headers; + request.content = content; + Some(request) } - fn sanitize_response_with_codec(&self, payload: Json) -> Option { - let codec = self.response_codec.as_ref()?; - let codec_name = self.codec_name?; + fn sanitize_request_parts( + &self, + headers: Map, + content: Json, + ) -> Option<(Map, Json)> { + let mut values = self.sanitize_json_roots(vec![ + (vec!["headers".to_string()], Json::Object(headers)), + (Vec::new(), content), + ]); + let content = values.pop()?; + let headers = values.pop()?.as_object()?.clone(); + Some((headers, content)) + } + + fn sanitize_response_with_codec( + &self, + codec: &dyn LlmResponseCodec, + surface: ProviderSurface, + payload: Json, + ) -> Option { + let codec_name = BuiltinCodecName::from_provider_surface(surface); let annotated = codec.decode_response(&payload).ok()?; let sanitized = sanitize_serializable(self, annotated).ok()?; Some(codec_name.overlay_response_payload(payload, &sanitized)) } - fn codec_failure_payload(&self, direction: &'static str) -> Json { + fn selected_surface(&self, codec: &LlmCodecIdentity) -> Option { + match codec { + LlmCodecIdentity::None => self.legacy_surface, + LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat) => { + Some(ProviderSurface::OpenAIChat) + } + LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiResponses) => { + Some(ProviderSurface::OpenAIResponses) + } + LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::AnthropicMessages) => { + Some(ProviderSurface::AnthropicMessages) + } + LlmCodecIdentity::Runtime(_) | LlmCodecIdentity::Opaque => None, + } + } + + fn uses_compatible_legacy_response_codec(&self, payload: &Json) -> bool { + self.legacy_surface + .is_some_and(|surface| detect_response_surface(payload) == Some(surface)) + } + + fn log_codec_failure(&self, direction: &'static str, codec: &LlmCodecIdentity, reason: &str) { + let codec_kind = match codec { + LlmCodecIdentity::None => "none", + LlmCodecIdentity::BuiltIn(_) => "builtin", + LlmCodecIdentity::Runtime(_) => "runtime", + LlmCodecIdentity::Opaque => "opaque", + }; log::warn!( target: "nemo_relay.plugin", event = "local_model_codec_failed", plugin_kind = "pii_redaction", provider = self.provider_name.as_str(), - direction; - "PII local-model codec failed closed" + direction, + codec_kind, + reason; + "PII local-model payload omitted after codec failure" ); - Json::String(self.replacement.as_str().to_string()) } } +fn compile_json_pointer(pointer: String) -> Vec { + pointer.strip_prefix('/').map_or_else(Vec::new, |path| { + path.split('/').map(str::to_string).collect() + }) +} + pub(super) fn register_local_backend( config: PiiRedactionConfig, ctx: &mut PluginRegistrationContext, @@ -595,41 +690,107 @@ fn event_sanitize_callback( && event .category() .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); - if !specialized_scope { - fields.data = fields.data.map(|data| backend.sanitize_json(data)); - fields.category_profile = fields.category_profile.and_then(|profile| { - sanitize_serializable::(&backend, profile).ok() - }); + + let mut selected = Vec::with_capacity(3); + if !specialized_scope && let Some(data) = fields.data.take() { + selected.push((EventField::Data, data)); + } + if !specialized_scope + && let Some(profile) = fields.category_profile.take() + && let Ok(profile) = serde_json::to_value(profile) + { + selected.push((EventField::CategoryProfile, profile)); + } + if let Some(metadata) = fields.metadata.take() { + selected.push((EventField::Metadata, metadata)); + } + + let values = selected + .iter_mut() + .map(|(_, value)| std::mem::take(value)) + .collect(); + for ((field, _), value) in selected + .into_iter() + .zip(backend.sanitize_json_values(values)) + { + match field { + EventField::Data => fields.data = Some(value), + EventField::CategoryProfile => { + fields.category_profile = serde_json::from_value(value).ok(); + } + EventField::Metadata => fields.metadata = Some(value), + } } - fields.metadata = fields - .metadata - .map(|metadata| backend.sanitize_json(metadata)); fields }) } fn llm_sanitize_request_callback(backend: CompiledLocalBackend) -> LlmSanitizeRequestFn { - Arc::new(move |mut request| { - if backend.request_codec.is_some() { - request.content = match backend.sanitize_request_with_codec(&request) { - Some(encoded) => return encoded, - None => backend.codec_failure_payload("request"), - }; - return request; + Arc::new(move |mut request, context| { + if backend.target_paths.is_empty() && backend.target_path_patterns.is_empty() { + request.content = backend.sanitize_json(request.content); + return Some(request); + } + if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { + return backend.sanitize_raw_request(request); + } + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + backend + .selected_surface(context.codec()) + .map(build_request_codec) + } else { + None + }; + let sanitized = resolved + .as_deref() + .or(fallback.as_deref()) + .and_then(|codec| backend.sanitize_request_with_codec(codec, &request)); + if sanitized.is_none() { + backend.log_codec_failure( + "request", + context.codec(), + "codec decode, sanitize, or encode failure", + ); } - request.content = backend.sanitize_json(request.content); - request + sanitized }) } fn llm_sanitize_response_callback(backend: CompiledLocalBackend) -> LlmSanitizeResponseFn { - Arc::new(move |payload| { - if backend.response_codec.is_some() { - return backend - .sanitize_response_with_codec(payload) - .unwrap_or_else(|| backend.codec_failure_payload("response")); + Arc::new(move |payload, context| { + if backend.target_paths.is_empty() && backend.target_path_patterns.is_empty() { + return Some(backend.sanitize_json(payload)); + } + if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { + return Some(backend.sanitize_json(payload)); + } + if matches!(context.codec(), LlmCodecIdentity::None) + && !backend.uses_compatible_legacy_response_codec(&payload) + { + backend.log_codec_failure("response", context.codec(), "no compatible legacy codec"); + return None; + } + let surface = backend.selected_surface(context.codec()); + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + surface.map(build_response_codec) + } else { + None + }; + let sanitized = surface + .zip(resolved.as_deref().or(fallback.as_deref())) + .and_then(|(surface, codec)| { + backend.sanitize_response_with_codec(codec, surface, payload) + }); + if sanitized.is_none() { + backend.log_codec_failure( + "response", + context.codec(), + "codec decode, sanitize, or encode failure", + ); } - backend.sanitize_json(payload) + sanitized }) } diff --git a/crates/pii-redaction/tests/unit/component_tests.rs b/crates/pii-redaction/tests/unit/component_tests.rs index a316732e4..51d86a0d6 100644 --- a/crates/pii-redaction/tests/unit/component_tests.rs +++ b/crates/pii-redaction/tests/unit/component_tests.rs @@ -317,6 +317,48 @@ impl LlmCodec for IdentifiedRequestCodec { } } +#[test] +fn raw_llm_paths_remain_usable_without_a_codec() { + let backend = crate::builtin::CompiledBuiltinBackend::new( + BuiltinBackendConfig { + action: "regex_replace".to_string(), + pattern: Some("sk-[A-Za-z0-9_-]+".to_string()), + replacement: Some("[REDACTED]".to_string()), + target_paths: vec!["/message".to_string()], + ..BuiltinBackendConfig::default() + }, + None, + ) + .unwrap(); + let sanitize_request = crate::builtin::llm_sanitize_request_callback(backend.clone()); + let sanitize_response = crate::builtin::llm_sanitize_response_callback(backend); + + let request = sanitize_request( + LlmRequest { + headers: serde_json::Map::new(), + content: json!({ + "message": "sk-request-secret", + "model": "sk-model-identifier" + }), + }, + no_codec_request_context(), + ) + .expect("raw request paths should not require a codec"); + assert_eq!(request.content["message"], "[REDACTED]"); + assert_eq!(request.content["model"], "sk-model-identifier"); + + let response = sanitize_response( + json!({ + "message": "sk-response-secret", + "model": "sk-model-identifier" + }), + no_codec_context(), + ) + .expect("raw response paths should not require a codec"); + assert_eq!(response["message"], "[REDACTED]"); + assert_eq!(response["model"], "sk-model-identifier"); +} + #[test] fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs() { let backend = crate::builtin::CompiledBuiltinBackend::new( diff --git a/crates/pii-redaction/tests/unit/local_tests.rs b/crates/pii-redaction/tests/unit/local_tests.rs index da3935c8c..4669b8422 100644 --- a/crates/pii-redaction/tests/unit/local_tests.rs +++ b/crates/pii-redaction/tests/unit/local_tests.rs @@ -3,6 +3,14 @@ use std::sync::atomic::{AtomicUsize, Ordering}; +use nemo_relay::api::event::{BaseEvent, CategoryProfile, Event, EventSanitizeFields, MarkEvent}; +use nemo_relay::api::runtime::{LlmSanitizeRequestContext, LlmSanitizeResponseContext}; +use nemo_relay::codec::openai_responses::OpenAIResponsesCodec; +use nemo_relay::codec::request::AnnotatedLlmRequest; +use nemo_relay::codec::resolve::{ + ProviderSurface, request_codec as build_request_codec, response_codec as build_response_codec, +}; +use nemo_relay::codec::traits::LlmCodec; use nemo_relay::plugin::{ InferenceProviderDescriptor, InferenceProviderRegistration, InferenceProviderRegistry, PluginRegistrationContext, @@ -15,6 +23,28 @@ struct ProviderGuard { _registration: InferenceProviderRegistration, } +struct IdentifiedRequestCodec { + identity: LlmCodecIdentity, +} + +impl LlmCodec for IdentifiedRequestCodec { + fn codec_identity(&self) -> LlmCodecIdentity { + self.identity.clone() + } + + fn decode(&self, request: &LlmRequest) -> nemo_relay::error::Result { + OpenAIResponsesCodec.decode(request) + } + + fn encode( + &self, + annotated: &AnnotatedLlmRequest, + original: &LlmRequest, + ) -> nemo_relay::error::Result { + OpenAIResponsesCodec.encode(annotated, original) + } +} + fn provider_context( name: &'static str, callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, @@ -184,6 +214,67 @@ fn batches_provider_requests_and_preserves_no_detection_values() { assert_eq!(calls.load(Ordering::SeqCst), 2); } +#[test] +fn batches_multiple_event_roots_into_one_provider_request() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let (_provider, backend) = backend("local-test-multi-root", move |request, _| { + observed.fetch_add(1, Ordering::SeqCst); + assert_eq!(request["texts"].as_array().unwrap().len(), 3); + Ok(json!({"version": 1, "detections": []})) + }); + + let sanitized = backend.sanitize_json_values(vec![ + json!({"message": "first"}), + json!({"name": "second"}), + json!({"trace": "third"}), + ]); + + assert_eq!( + sanitized, + vec![ + json!({"message": "first"}), + json!({"name": "second"}), + json!({"trace": "third"}), + ] + ); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[test] +fn event_callback_batches_all_selected_fields_into_one_provider_request() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let (_provider, backend) = backend("local-test-event-batching", move |request, _| { + observed.fetch_add(1, Ordering::SeqCst); + assert_eq!(request["texts"].as_array().unwrap().len(), 3); + Ok(json!({"version": 1, "detections": []})) + }); + let callback = event_sanitize_callback(backend, None); + let event = Event::Mark(MarkEvent::new( + BaseEvent::builder().name("mark").build(), + None, + None, + )); + + let sanitized = callback( + &event, + EventSanitizeFields { + data: Some(json!({"message": "first"})), + category_profile: Some(CategoryProfile::builder().subtype("second").build()), + metadata: Some(json!({"trace": "third"})), + }, + ); + + assert_eq!(sanitized.data.unwrap()["message"], "first"); + assert_eq!( + sanitized.category_profile.unwrap().subtype.as_deref(), + Some("second") + ); + assert_eq!(sanitized.metadata.unwrap()["trace"], "third"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + #[test] fn latency_budget_applies_to_the_entire_payload() { let calls = Arc::new(AtomicUsize::new(0)); @@ -430,6 +521,90 @@ fn target_path_patterns_match_one_segment_without_widening_exact_paths() { ); } +#[test] +fn exact_paths_match_escaped_object_keys() { + let (_provider, ctx) = provider_context("local-test-escaped-path", alice_detector); + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-escaped-path".into()), + target_paths: vec!["/a~1b/~0name".into()], + ..LocalBackendConfig::default() + }, + None, + &ctx, + ) + .unwrap(); + + assert_eq!( + backend.sanitize_json(json!({ + "a/b": {"~name": "Alice", "name": "Alice"}, + "a~1b": {"~name": "Alice"} + })), + json!({ + "a/b": {"~name": "[REDACTED]", "name": "Alice"}, + "a~1b": {"~name": "Alice"} + }) + ); +} + +#[test] +fn raw_request_paths_batch_headers_and_content_without_a_codec() { + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let (_provider, ctx) = provider_context("local-test-raw-request", move |request, timeout| { + observed.fetch_add(1, Ordering::SeqCst); + alice_detector(request, timeout) + }); + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-raw-request".into()), + target_paths: vec!["/headers/x-user".into(), "/message".into()], + ..LocalBackendConfig::default() + }, + None, + &ctx, + ) + .unwrap(); + + let sanitized = llm_sanitize_request_callback(backend)( + LlmRequest { + headers: Map::from_iter([("x-user".into(), json!("Alice"))]), + content: json!({"message": "Hello Alice", "model": "Alice-model"}), + }, + LlmSanitizeRequestContext::default(), + ) + .expect("raw request paths should not require a codec"); + + assert_eq!(sanitized.headers["x-user"], "[REDACTED]"); + assert_eq!(sanitized.content["message"], "Hello [REDACTED]"); + assert_eq!(sanitized.content["model"], "Alice-model"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[test] +fn raw_response_paths_work_without_a_codec() { + let (_provider, ctx) = provider_context("local-test-raw-response", alice_detector); + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-raw-response".into()), + target_paths: vec!["/message".into()], + ..LocalBackendConfig::default() + }, + None, + &ctx, + ) + .unwrap(); + + let sanitized = llm_sanitize_response_callback(backend)( + json!({"message": "Hello Alice", "model": "Alice-model"}), + LlmSanitizeResponseContext::default(), + ) + .expect("raw response paths should not require a codec"); + + assert_eq!(sanitized["message"], "Hello [REDACTED]"); + assert_eq!(sanitized["model"], "Alice-model"); +} + #[test] fn request_codec_classifies_only_normalized_content_patterns() { let (_provider, ctx) = provider_context("local-test-openai-request", alice_detector); @@ -463,10 +638,7 @@ fn request_codec_classifies_only_normalized_content_patterns() { ] }), }; - let codec = backend - .request_codec - .as_ref() - .expect("configured request codec should exist"); + let codec = build_request_codec(ProviderSurface::OpenAIChat); let annotated = codec .decode(&request) .expect("OpenAI request should decode"); @@ -476,7 +648,11 @@ fn request_codec_classifies_only_normalized_content_patterns() { .encode(&sanitized_annotated, &request) .expect("sanitized OpenAI request should encode"); - let sanitized = llm_sanitize_request_callback(backend)(request); + let sanitized = llm_sanitize_request_callback(backend)( + request, + LlmSanitizeRequestContext::for_request_codec(Some(codec)), + ) + .expect("valid request should remain observable"); assert_eq!(sanitized.content["model"], "Alice-model"); assert_eq!(sanitized.content["trace_id"], "Alice-trace"); @@ -494,6 +670,45 @@ fn request_codec_classifies_only_normalized_content_patterns() { ); } +#[test] +fn request_uses_the_active_codec_instead_of_the_legacy_fallback() { + let (_provider, ctx) = provider_context("local-test-active-request-codec", alice_detector); + let backend = CompiledLocalBackend::new( + LocalBackendConfig { + backend: Some("local-test-active-request-codec".into()), + target_path_patterns: vec!["/messages/*/content".into()], + ..LocalBackendConfig::default() + }, + Some("openai_chat".into()), + &ctx, + ) + .unwrap(); + let sanitize = llm_sanitize_request_callback(backend); + let request = || LlmRequest { + headers: serde_json::Map::new(), + content: json!({ + "model": "test-model", + "input": [{"role": "user", "content": "Email Alice"}] + }), + }; + + for codec in [ + Arc::new(IdentifiedRequestCodec { + identity: LlmCodecIdentity::Runtime("test.responses.v1".into()), + }) as Arc, + Arc::new(IdentifiedRequestCodec { + identity: LlmCodecIdentity::Opaque, + }) as Arc, + ] { + let sanitized = sanitize( + request(), + LlmSanitizeRequestContext::for_request_codec(Some(codec)), + ) + .expect("active runtime codecs should remain usable"); + assert_eq!(sanitized.content["input"][0]["content"], "Email [REDACTED]"); + } +} + #[test] fn response_codec_classifies_message_content_without_touching_identity_fields() { let (_provider, ctx) = provider_context("local-test-openai-response", alice_detector); @@ -519,7 +734,11 @@ fn response_codec_classifies_message_content_without_touching_identity_fields() }); let sanitized = backend - .sanitize_response_with_codec(response) + .sanitize_response_with_codec( + build_response_codec(ProviderSurface::OpenAIChat).as_ref(), + ProviderSurface::OpenAIChat, + response, + ) .expect("configured codec should sanitize the response"); assert_eq!(sanitized["id"], "Alice-response"); @@ -532,7 +751,7 @@ fn response_codec_classifies_message_content_without_touching_identity_fields() } #[test] -fn request_codec_failure_replaces_the_observable_body() { +fn request_codec_failure_omits_the_observable_body() { let (_provider, ctx) = provider_context("local-test-invalid-openai-request", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { @@ -556,10 +775,14 @@ fn request_codec_failure_replaces_the_observable_body() { }), }; - let sanitized = llm_sanitize_request_callback(backend)(request); + let sanitized = llm_sanitize_request_callback(backend)( + request, + LlmSanitizeRequestContext::for_request_codec(Some(build_request_codec( + ProviderSurface::OpenAIChat, + ))), + ); - assert_eq!(sanitized.content, json!("[PRIVATE]")); - assert_eq!(sanitized.headers["x-provider-id"], "preserve-header"); + assert!(sanitized.is_none()); } #[test] @@ -586,13 +809,18 @@ fn request_codec_ambiguous_multi_message_edit_fails_closed() { }), }; - let sanitized = llm_sanitize_request_callback(backend)(request); + let sanitized = llm_sanitize_request_callback(backend)( + request, + LlmSanitizeRequestContext::for_request_codec(Some(build_request_codec( + ProviderSurface::OpenAIChat, + ))), + ); - assert_eq!(sanitized.content, json!("[PRIVATE]")); + assert!(sanitized.is_none()); } #[test] -fn response_codec_failure_replaces_the_observable_payload() { +fn response_codec_failure_omits_the_observable_payload() { let (_provider, ctx) = provider_context("local-test-invalid-openai-response", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { @@ -610,9 +838,14 @@ fn response_codec_failure_replaces_the_observable_payload() { "vendor_trace": "Alice-trace" }); - let sanitized = llm_sanitize_response_callback(backend)(response); + let sanitized = llm_sanitize_response_callback(backend)( + response, + LlmSanitizeResponseContext::for_response_codec(Some(build_response_codec( + ProviderSurface::OpenAIChat, + ))), + ); - assert_eq!(sanitized, json!("[PRIVATE]")); + assert!(sanitized.is_none()); } #[test] diff --git a/docs/configure-plugins/pii-redaction/about.mdx b/docs/configure-plugins/pii-redaction/about.mdx index cd2ecca78..3c6bd7f65 100644 --- a/docs/configure-plugins/pii-redaction/about.mdx +++ b/docs/configure-plugins/pii-redaction/about.mdx @@ -123,8 +123,8 @@ For managed LLM requests, codec decode and re-encode can canonicalize the emitted provider-shaped start event. For example, Relay can record an OpenAI Responses request in the codec's canonical `input` array form rather than the original shorthand form. If codec processing fails, the local-model backend -replaces the entire emitted LLM body rather than applying normalized selectors -to an incompatible raw provider shape. +omits the emitted LLM body rather than applying normalized selectors to an +incompatible raw provider shape. ## Current Boundaries diff --git a/docs/configure-plugins/pii-redaction/configuration.mdx b/docs/configure-plugins/pii-redaction/configuration.mdx index 95934911c..5bb3e4a31 100644 --- a/docs/configure-plugins/pii-redaction/configuration.mdx +++ b/docs/configure-plugins/pii-redaction/configuration.mdx @@ -362,9 +362,9 @@ a process sandbox. Provider failures, timeouts, malformed responses, invalid UTF-8 spans, overlapping spans, and input-limit violations fail closed for the affected batch. If a configured codec cannot decode or safely re-encode an LLM payload, -Relay replaces the entire emitted request or response body. The default -deadline is 250 ms for the complete selected payload, including every provider -batch. Configuration above 60 seconds is rejected. +Relay omits that request or response payload from the emitted event. The +default deadline is 250 ms for the complete selected payload, including every +provider batch. Configuration above 60 seconds is rejected. Use `profiles` to run deterministic recognizers before a contextual model: @@ -387,7 +387,7 @@ priority = 90 [components.config.profiles.local] backend = "nemo_relay.pii_rampart/detector" min_score = 0.4 -max_latency_ms = 1500 +max_latency_ms = 5000 target_path_patterns = [ "/messages/*/content", "/messages/*/content/*/text", @@ -403,8 +403,9 @@ policy boundary. The generic local-model payload deadline defaults to 250 ms. Contextual models can need more time for large selected payloads. The Rampart profile above uses -1500 ms so a request near the 64 KiB provider limit has practical headroom; -benchmark representative inputs on deployment hardware before lowering it. +5000 ms so a request near the 64 KiB provider limit has practical headroom on +typical client hardware; benchmark representative inputs on deployment +hardware before lowering it. The optional Rampart provider is distributed as a manifest-backed Python source bundle under `crates/pii-redaction/providers/rampart`. Prefetch its pinned model diff --git a/go/nemo_relay/pii_redaction/pii_redaction_test.go b/go/nemo_relay/pii_redaction/pii_redaction_test.go index dc78be99f..95c62a127 100644 --- a/go/nemo_relay/pii_redaction/pii_redaction_test.go +++ b/go/nemo_relay/pii_redaction/pii_redaction_test.go @@ -28,11 +28,19 @@ func TestPiiRedactionComponentSpecAndLocalModelHelpers(t *testing.T) { config := NewConfig() local := NewLocalModelConfig() minScore := 0.75 + replacement := "[PRIVATE]" + allowNetwork := false + maxLatencyMS := int32(250) local.Backend = "nemo_relay.pii_rampart/detector" local.ModelID = "pii-model" + local.DetectorProfile = "default" + local.TargetPaths = []string{"/message"} local.TargetPathPatterns = []string{"/messages/*/content"} local.MinScore = &minScore local.ExcludedLabels = []string{"CITY"} + local.Replacement = &replacement + local.AllowNetwork = &allowNetwork + local.MaxLatencyMS = &maxLatencyMS config.Mode = "local_model" config.Local = &local @@ -40,10 +48,18 @@ func TestPiiRedactionComponentSpecAndLocalModelHelpers(t *testing.T) { if !spec.Enabled || spec.Config.Local == nil || spec.Config.Local.ModelID != "pii-model" || + spec.Config.Local.DetectorProfile != "default" || + len(spec.Config.Local.TargetPaths) != 1 || len(spec.Config.Local.TargetPathPatterns) != 1 || spec.Config.Local.MinScore == nil || *spec.Config.Local.MinScore != minScore || - len(spec.Config.Local.ExcludedLabels) != 1 { + len(spec.Config.Local.ExcludedLabels) != 1 || + spec.Config.Local.Replacement == nil || + *spec.Config.Local.Replacement != replacement || + spec.Config.Local.AllowNetwork == nil || + *spec.Config.Local.AllowNetwork || + spec.Config.Local.MaxLatencyMS == nil || + *spec.Config.Local.MaxLatencyMS != maxLatencyMS { t.Fatalf("unexpected PII redaction component spec: %#v", spec) } } diff --git a/python/tests/test_pii_redaction_plugin.py b/python/tests/test_pii_redaction_plugin.py index 1e0debc8b..ab57329b3 100644 --- a/python/tests/test_pii_redaction_plugin.py +++ b/python/tests/test_pii_redaction_plugin.py @@ -32,6 +32,8 @@ def test_defaults_and_component_wrapper(self): assert LocalModelConfig().to_dict() == {} assert LocalModelConfig( backend="acme.pii/detector", + model_id="pii-model-v1", + detector_profile="default", target_paths=["/message"], target_path_patterns=["/messages/*/content"], min_score=0.6, @@ -41,6 +43,8 @@ def test_defaults_and_component_wrapper(self): max_latency_ms=250, ).to_dict() == { "backend": "acme.pii/detector", + "model_id": "pii-model-v1", + "detector_profile": "default", "target_paths": ["/message"], "target_path_patterns": ["/messages/*/content"], "min_score": 0.6, From f513f562e5aaff14b1678273106bf546f41410f5 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sun, 26 Jul 2026 16:08:10 -0700 Subject: [PATCH 06/83] fix(pii): bound provider metadata and reduce allocations Signed-off-by: Alex Fournier --- .../nemo_relay_pii_rampart/detector.py | 20 +++++++------ .../providers/rampart/tests/test_detector.py | 29 +++++++++++++++++++ crates/pii-redaction/src/local.rs | 8 +++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py index 8f8844229..affc1353c 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py @@ -29,6 +29,7 @@ CONTENT_TOKEN_BUDGET = MODEL_MAX_TOKENS - SPECIAL_TOKEN_COUNT WINDOW_OVERLAP_TOKENS = 64 MAX_MODEL_REFERENCE_BYTES = 1024 +MAX_DETECTOR_PROFILE_BYTES = 1024 MAX_CACHE_PATH_BYTES = 4096 _MODEL_FILE_SHA256 = { @@ -209,10 +210,9 @@ def load(cls, settings: RampartSettings) -> RampartDetector: def detect_request(self, request: Any) -> dict[str, Any]: """Validate one provider request and return versioned UTF-8 spans.""" - texts, requested_model = _parse_request(request) + texts, requested_model, profile = _parse_request(request) if requested_model is not None and requested_model != DEFAULT_MODEL_ID: raise ValueError(f"request model_id {requested_model!r} does not match loaded model {DEFAULT_MODEL_ID!r}") - profile = request.get("detector_profile") if profile not in (None, "default"): raise ValueError(f"unsupported detector_profile {profile!r}") @@ -243,10 +243,12 @@ def _detect_texts(self, texts: list[_InputText]) -> list[dict[str, Any]]: detections = [] text_by_id = {item.text_id: item.text for item in texts} - byte_offsets_by_id = {text_id: _utf8_offsets(text) for text_id, text in text_by_id.items()} for text_id, spans in spans_by_text.items(): - for span in _merge_overlapping_spans(spans): - byte_offsets = byte_offsets_by_id[text_id] + merged_spans = _merge_overlapping_spans(spans) + if not merged_spans: + continue + byte_offsets = _utf8_offsets(text_by_id[text_id]) + for span in merged_spans: detections.append( { "text_id": text_id, @@ -428,7 +430,7 @@ def _uses_explicit_model_path(model_id: str) -> bool: return Path(model_id).expanduser().is_absolute() or model_id.startswith(("./", "../", ".\\", "..\\", "~/", "~\\")) -def _parse_request(request: Any) -> tuple[list[_InputText], str | None]: +def _parse_request(request: Any) -> tuple[list[_InputText], str | None, str | None]: if not isinstance(request, dict): raise TypeError("local-model request must be a JSON object") allowed = {"version", "model_id", "detector_profile", "texts"} @@ -440,10 +442,10 @@ def _parse_request(request: Any) -> tuple[list[_InputText], str | None]: raise ValueError(f"local-model request version must be {CONTRACT_VERSION}") model_id = request.get("model_id") if model_id is not None: - model_id = _nonempty_string(model_id, "model_id") + model_id = _bounded_string(model_id, "model_id", MAX_MODEL_REFERENCE_BYTES) profile = request.get("detector_profile") if profile is not None: - _nonempty_string(profile, "detector_profile") + profile = _bounded_string(profile, "detector_profile", MAX_DETECTOR_PROFILE_BYTES) raw_texts = request.get("texts") if not isinstance(raw_texts, list) or not raw_texts: raise TypeError("texts must be a non-empty array") @@ -472,7 +474,7 @@ def _parse_request(request: Any) -> tuple[list[_InputText], str | None]: raise ValueError(f"request text exceeds {MAX_REQUEST_TEXT_BYTES} UTF-8 bytes") seen_ids.add(text_id) texts.append(_InputText(text_id, text)) - return texts, model_id + return texts, model_id, profile def _split_bio_label(raw_label: str | None) -> tuple[str | None, str | None]: diff --git a/crates/pii-redaction/providers/rampart/tests/test_detector.py b/crates/pii-redaction/providers/rampart/tests/test_detector.py index 9614549d7..cc54c960a 100644 --- a/crates/pii-redaction/providers/rampart/tests/test_detector.py +++ b/crates/pii-redaction/providers/rampart/tests/test_detector.py @@ -179,6 +179,35 @@ def test_request_validation_rejects_duplicate_ids_and_byte_overflow() -> None: ) with pytest.raises(ValueError, match="UTF-8 bytes"): _parse_request({"version": 1, "texts": [{"id": 0, "text": "é" * 9000}]}) + with pytest.raises(ValueError, match="model_id"): + _parse_request( + { + "version": 1, + "model_id": "x" * 1025, + "texts": [{"id": 0, "text": ""}], + } + ) + with pytest.raises(ValueError, match="detector_profile"): + _parse_request( + { + "version": 1, + "detector_profile": "x" * 1025, + "texts": [{"id": 0, "text": ""}], + } + ) + + +def test_no_detection_does_not_allocate_utf8_offset_tables(monkeypatch: pytest.MonkeyPatch) -> None: + def unexpected_offsets(_text: str) -> list[int]: + raise AssertionError("UTF-8 offsets should be lazy when no spans were detected") + + monkeypatch.setattr(detector_module, "_utf8_offsets", unexpected_offsets) + assert detector([], []).detect_request( + { + "version": 1, + "texts": [{"id": 0, "text": "no private values"}], + } + ) == {"version": 1, "detections": []} def test_detector_returns_utf8_byte_offsets_and_model_labels() -> None: diff --git a/crates/pii-redaction/src/local.rs b/crates/pii-redaction/src/local.rs index 17dd3c019..d60c56972 100644 --- a/crates/pii-redaction/src/local.rs +++ b/crates/pii-redaction/src/local.rs @@ -434,7 +434,9 @@ impl CompiledLocalBackend { detection.text_id ))); } - if detection.label.trim().is_empty() || detection.label.len() > 128 { + if detection.label.trim().is_empty() + || detection.label.len() > MAX_LOCAL_MODEL_LABEL_BYTES + { return Err(PluginError::RegistrationFailed( "local-model response contained an invalid detection label".into(), )); @@ -524,7 +526,9 @@ impl CompiledLocalBackend { (Vec::new(), content), ]); let content = values.pop()?; - let headers = values.pop()?.as_object()?.clone(); + let Json::Object(headers) = values.pop()? else { + return None; + }; Some((headers, content)) } From 385c241213befca95172499f09f08f243f714a05 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sun, 26 Jul 2026 16:08:18 -0700 Subject: [PATCH 07/83] refactor(pii): centralize backend config fields Signed-off-by: Alex Fournier --- crates/pii-redaction/src/component.rs | 76 ++++++++++----------------- 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/crates/pii-redaction/src/component.rs b/crates/pii-redaction/src/component.rs index 21b90a223..bddd024c2 100644 --- a/crates/pii-redaction/src/component.rs +++ b/crates/pii-redaction/src/component.rs @@ -39,6 +39,30 @@ pub(super) const MAX_LOCAL_MODEL_REPLACEMENT_BYTES: usize = 1024; pub(super) const MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES: usize = 1024; pub(super) const MAX_LOCAL_MODEL_EXCLUDED_LABELS: usize = 128; pub(super) const MAX_LOCAL_MODEL_LABEL_BYTES: usize = 128; +const BUILTIN_BACKEND_CONFIG_FIELDS: &[&str] = &[ + "preset", + "action", + "target_paths", + "pattern", + "detector", + "replacement", + "mask_char", + "unmasked_prefix", + "unmasked_suffix", + "custom_mark_payload_policy", +]; +const LOCAL_BACKEND_CONFIG_FIELDS: &[&str] = &[ + "backend", + "model_id", + "detector_profile", + "target_paths", + "target_path_patterns", + "min_score", + "excluded_labels", + "replacement", + "allow_network", + "max_latency_ms", +]; /// Top-level PII redaction component wrapper. #[derive(Debug, Clone)] @@ -680,36 +704,14 @@ fn validate_pii_redaction_plugin_config_with_policy( &config.policy, plugin_config, "builtin", - &[ - "preset", - "action", - "target_paths", - "pattern", - "detector", - "replacement", - "mask_char", - "unmasked_prefix", - "unmasked_suffix", - "custom_mark_payload_policy", - ], + BUILTIN_BACKEND_CONFIG_FIELDS, ); validate_section_fields( &mut diagnostics, &config.policy, plugin_config, "local", - &[ - "backend", - "model_id", - "detector_profile", - "target_paths", - "target_path_patterns", - "min_score", - "excluded_labels", - "replacement", - "allow_network", - "max_latency_ms", - ], + LOCAL_BACKEND_CONFIG_FIELDS, ); validate_version(&mut diagnostics, &config.policy, config.version); validate_mode(&mut diagnostics, &config.policy, &config); @@ -788,36 +790,14 @@ fn validate_profile_configuration( &config.policy, raw_profile, "builtin", - &[ - "preset", - "action", - "target_paths", - "pattern", - "detector", - "replacement", - "mask_char", - "unmasked_prefix", - "unmasked_suffix", - "custom_mark_payload_policy", - ], + BUILTIN_BACKEND_CONFIG_FIELDS, ); validate_section_fields( &mut profile_diagnostics, &config.policy, raw_profile, "local", - &[ - "backend", - "model_id", - "detector_profile", - "target_paths", - "target_path_patterns", - "min_score", - "excluded_labels", - "replacement", - "allow_network", - "max_latency_ms", - ], + LOCAL_BACKEND_CONFIG_FIELDS, ); validate_mode(&mut profile_diagnostics, &config.policy, &profile_config); validate_builtin_mode_requirements( From 8777e3f74a7121689929b1b70b3e373842a87677 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 27 Jul 2026 10:25:11 -0700 Subject: [PATCH 08/83] fix(pii): bound padded inference batches Signed-off-by: Alex Fournier --- .../pii-redaction/providers/rampart/README.md | 4 +++ .../providers/rampart/config.schema.json | 3 +- .../nemo_relay_pii_rampart/detector.py | 23 +++++++++++-- .../providers/rampart/tests/test_detector.py | 34 +++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/crates/pii-redaction/providers/rampart/README.md b/crates/pii-redaction/providers/rampart/README.md index ae1458a94..91d49d4b6 100644 --- a/crates/pii-redaction/providers/rampart/README.md +++ b/crates/pii-redaction/providers/rampart/README.md @@ -69,6 +69,10 @@ inference_batch_size = 16 max_pending_requests = 8 ``` +`inference_batch_size` is an upper bound. The worker batches short token +windows together but automatically reduces the batch width for longer windows +to bound ONNX intermediate memory. + Add the PII component to the same `plugins.toml`: ```toml diff --git a/crates/pii-redaction/providers/rampart/config.schema.json b/crates/pii-redaction/providers/rampart/config.schema.json index 769119073..43fac3687 100644 --- a/crates/pii-redaction/providers/rampart/config.schema.json +++ b/crates/pii-redaction/providers/rampart/config.schema.json @@ -36,7 +36,8 @@ "type": "integer", "minimum": 1, "maximum": 64, - "default": 16 + "default": 16, + "description": "Maximum number of token windows per ONNX call. The worker reduces this automatically to bound padded token volume." }, "max_pending_requests": { "type": "integer", diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py index affc1353c..6dd482516 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py +++ b/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py @@ -8,7 +8,7 @@ import hashlib import json import threading -from collections.abc import Mapping +from collections.abc import Iterator, Mapping from dataclasses import dataclass from pathlib import Path from typing import Any, Protocol @@ -28,6 +28,7 @@ SPECIAL_TOKEN_COUNT = 2 CONTENT_TOKEN_BUDGET = MODEL_MAX_TOKENS - SPECIAL_TOKEN_COUNT WINDOW_OVERLAP_TOKENS = 64 +MAX_PADDED_TOKENS_PER_BATCH = MODEL_MAX_TOKENS MAX_MODEL_REFERENCE_BYTES = 1024 MAX_DETECTOR_PROFILE_BYTES = 1024 MAX_CACHE_PATH_BYTES = 4096 @@ -235,8 +236,7 @@ def _validate_model_contract(self) -> None: def _detect_texts(self, texts: list[_InputText]) -> list[dict[str, Any]]: windows = self._build_windows(texts) spans_by_text: dict[int, list[_Span]] = {item.text_id: [] for item in texts} - for start in range(0, len(windows), self.settings.inference_batch_size): - batch = windows[start : start + self.settings.inference_batch_size] + for batch in _inference_batches(windows, self.settings.inference_batch_size): logits = self._infer(batch) for window, window_logits in zip(batch, logits, strict=True): spans_by_text[window.text_id].extend(self._decode_window(window, window_logits)) @@ -365,6 +365,23 @@ def finish() -> None: return spans +def _inference_batches(windows: list[_Window], max_batch_size: int) -> Iterator[list[_Window]]: + ordered = sorted(windows, key=lambda window: len(window.input_ids)) + batch: list[_Window] = [] + max_tokens = 0 + for window in ordered: + next_max_tokens = max(max_tokens, len(window.input_ids)) + padded_tokens = (len(batch) + 1) * next_max_tokens + if batch and (len(batch) >= max_batch_size or padded_tokens > MAX_PADDED_TOKENS_PER_BATCH): + yield batch + batch = [] + max_tokens = 0 + batch.append(window) + max_tokens = max(max_tokens, len(window.input_ids)) + if batch: + yield batch + + def _resolve_model_root(settings: RampartSettings) -> Path: local_path = _explicit_model_root(settings.model_id) if local_path is not None: diff --git a/crates/pii-redaction/providers/rampart/tests/test_detector.py b/crates/pii-redaction/providers/rampart/tests/test_detector.py index cc54c960a..d1d75a575 100644 --- a/crates/pii-redaction/providers/rampart/tests/test_detector.py +++ b/crates/pii-redaction/providers/rampart/tests/test_detector.py @@ -75,6 +75,16 @@ def run(self, output_names: list[str], input_feed: dict[str, np.ndarray]) -> lis return [logits] +class RecordingSession(FakeSession): + def __init__(self) -> None: + super().__init__([], []) + self.shapes: list[tuple[int, int]] = [] + + def run(self, output_names: list[str], input_feed: dict[str, np.ndarray]) -> list[np.ndarray]: + self.shapes.append(input_feed["input_ids"].shape) + return super().run(output_names, input_feed) + + def detector( label_ids: list[int], scores: list[float], @@ -286,6 +296,30 @@ def test_request_window_limit_is_enforced_before_inference() -> None: current.detect_request({"version": 1, "texts": [{"id": 0, "text": text}]}) +def test_inference_batches_short_windows_and_isolates_full_windows() -> None: + session = RecordingSession() + current = RampartDetector( + RampartSettings.from_config({"inference_batch_size": 16}), + FakeTokenizer(), + session, + {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, + ) + full_window = " ".join("word" for _ in range(detector_module.CONTENT_TOKEN_BUDGET)) + texts = [{"id": index, "text": "short"} for index in range(16)] + texts.extend( + [ + {"id": 16, "text": full_window}, + {"id": 17, "text": full_window}, + ] + ) + + assert current.detect_request({"version": 1, "texts": texts}) == { + "version": 1, + "detections": [], + } + assert session.shapes == [(16, 3), (1, detector_module.MODEL_MAX_TOKENS), (1, detector_module.MODEL_MAX_TOKENS)] + + def test_detector_rejects_invalid_model_outputs() -> None: with pytest.raises(ValueError, match="contiguous"): RampartDetector( From 7541becbd22cf55e8c09eb13c593cad434923c6b Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 27 Jul 2026 11:36:00 -0700 Subject: [PATCH 09/83] refactor(plugin): clarify worker inference boundary Signed-off-by: Alex Fournier --- crates/cli/src/server/mod.rs | 8 +- crates/core/src/lib.rs | 6 +- crates/core/src/plugin.rs | 92 ++++---- crates/core/src/plugin/dynamic/host.rs | 24 +- crates/core/src/plugin/dynamic/worker.rs | 94 ++++---- crates/core/src/plugin/inference.rs | 219 ------------------ crates/core/src/plugin/worker_inference.rs | 213 +++++++++++++++++ .../tests/fixtures/worker_plugin/src/main.rs | 20 +- .../tests/integration/worker_plugin_tests.rs | 126 +++++----- .../core/tests/unit/dynamic_worker_tests.rs | 12 +- crates/core/tests/unit/plugin_tests.rs | 52 ++--- ...der_tests.rs => worker_inference_tests.rs} | 55 +++-- crates/pii-redaction/README.md | 24 +- crates/pii-redaction/src/component.rs | 20 +- crates/pii-redaction/src/local.rs | 42 ++-- .../tests/unit/component_tests.rs | 101 ++++---- .../pii-redaction/tests/unit/local_tests.rs | 165 +++++++------ ...der_tests.rs => worker_detection_tests.rs} | 39 ++-- .../rampart/MANIFEST.in | 0 .../{providers => workers}/rampart/README.md | 16 +- .../rampart/THIRD_PARTY_NOTICES.md | 4 +- .../rampart/config.schema.json | 2 +- .../nemo_relay_pii_rampart/__init__.py | 2 +- .../nemo_relay_pii_rampart/detector.py | 2 +- .../nemo_relay_pii_rampart/prefetch.py | 0 .../rampart/nemo_relay_pii_rampart/py.typed | 0 .../rampart/nemo_relay_pii_rampart/worker.py | 12 +- .../rampart/pyproject.toml | 2 +- .../rampart/relay-plugin.toml | 2 +- .../rampart/tests/test_detector.py | 0 .../rampart/tests/test_worker.py | 6 +- crates/worker-proto/README.md | 2 +- .../nemo/relay/worker/v1/plugin_worker.proto | 4 +- crates/worker-proto/tests/proto_tests.rs | 18 +- crates/worker/README.md | 12 +- crates/worker/src/lib.rs | 34 +-- crates/worker/tests/worker_sdk_tests.rs | 20 +- docs/about-nemo-relay/release-notes/index.mdx | 4 +- .../grpc-worker/grpc-worker-protocol.mdx | 12 +- .../grpc-worker/python/about.mdx | 6 +- .../grpc-worker/rust/about.mdx | 6 +- .../configure-plugins/pii-redaction/about.mdx | 6 +- .../pii-redaction/configuration.mdx | 22 +- .../worker.py | 11 - .../relay-plugin.toml | 2 +- justfile | 4 +- python/plugin/README.md | 10 +- .../plugin/src/nemo_relay_plugin/__init__.py | 6 +- python/plugin/src/nemo_relay_plugin/_api.py | 34 +-- .../plugin/test_public_api_docstrings.py | 2 +- python/tests/plugin/test_worker_sdk.py | 24 +- 51 files changed, 782 insertions(+), 817 deletions(-) delete mode 100644 crates/core/src/plugin/inference.rs create mode 100644 crates/core/src/plugin/worker_inference.rs rename crates/core/tests/unit/{inference_provider_tests.rs => worker_inference_tests.rs} (53%) rename crates/pii-redaction/tests/{worker_provider_tests.rs => worker_detection_tests.rs} (91%) rename crates/pii-redaction/{providers => workers}/rampart/MANIFEST.in (100%) rename crates/pii-redaction/{providers => workers}/rampart/README.md (90%) rename crates/pii-redaction/{providers => workers}/rampart/THIRD_PARTY_NOTICES.md (82%) rename crates/pii-redaction/{providers => workers}/rampart/config.schema.json (96%) rename crates/pii-redaction/{providers => workers}/rampart/nemo_relay_pii_rampart/__init__.py (84%) rename crates/pii-redaction/{providers => workers}/rampart/nemo_relay_pii_rampart/detector.py (99%) rename crates/pii-redaction/{providers => workers}/rampart/nemo_relay_pii_rampart/prefetch.py (100%) rename crates/pii-redaction/{providers => workers}/rampart/nemo_relay_pii_rampart/py.typed (100%) rename crates/pii-redaction/{providers => workers}/rampart/nemo_relay_pii_rampart/worker.py (90%) rename crates/pii-redaction/{providers => workers}/rampart/pyproject.toml (94%) rename crates/pii-redaction/{providers => workers}/rampart/relay-plugin.toml (87%) rename crates/pii-redaction/{providers => workers}/rampart/tests/test_detector.py (100%) rename crates/pii-redaction/{providers => workers}/rampart/tests/test_worker.py (96%) diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index 2bfc568f9..87a1b2e40 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -25,7 +25,7 @@ use nemo_relay::plugin::dynamic::{ }; use nemo_relay::plugin::{ PluginComponentSpec, PluginConfig, clear_plugin_configuration, initialize_plugins_exact, - initialize_plugins_exact_with_inference_providers, + initialize_plugins_exact_with_worker_inference, }; use nemo_relay_adaptive::plugin_component::register_adaptive_component; use nemo_relay_pii_redaction::component::register_pii_redaction_component; @@ -1117,11 +1117,11 @@ impl PluginActivation { CliError::Config(format!("worker plugin load failed: {error}")) })?) }; - let inference_providers = worker + let worker_inference = worker .as_ref() - .map(WorkerPluginActivation::inference_providers) + .map(WorkerPluginActivation::worker_inference_registry) .unwrap_or_default(); - initialize_plugins_exact_with_inference_providers(plugin_config, inference_providers) + initialize_plugins_exact_with_worker_inference(plugin_config, worker_inference) .await .map_err(|error| CliError::Config(format!("plugin activation failed: {error}")))?; Ok(Self { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 6505e1e5f..6c805258d 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -69,9 +69,9 @@ mod registry; pub mod shared_runtime; pub mod stream; -#[cfg(test)] -#[path = "../tests/unit/inference_provider_tests.rs"] -mod inference_provider_tests; #[cfg(test)] #[path = "../tests/unit/types_tests.rs"] mod types_tests; +#[cfg(test)] +#[path = "../tests/unit/worker_inference_tests.rs"] +mod worker_inference_tests; diff --git a/crates/core/src/plugin.rs b/crates/core/src/plugin.rs index 0a8305d8a..d3133edc7 100644 --- a/crates/core/src/plugin.rs +++ b/crates/core/src/plugin.rs @@ -48,11 +48,11 @@ pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel}; pub mod dynamic; pub use dynamic::*; -mod inference; +mod worker_inference; #[doc(hidden)] -pub use inference::{ - InferenceProvider, InferenceProviderDescriptor, InferenceProviderFn, - InferenceProviderRegistration, InferenceProviderRegistry, +pub use worker_inference::{ + WorkerInference, WorkerInferenceDescriptor, WorkerInferenceFn, WorkerInferenceRegistration, + WorkerInferenceRegistry, }; type PluginMap = HashMap; @@ -348,7 +348,7 @@ impl PluginRegistration { pub struct PluginRegistrationContext { registrations: Vec, namespace: Option, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, } impl PluginRegistrationContext { @@ -362,31 +362,27 @@ impl PluginRegistrationContext { Self { registrations: vec![], namespace: Some(namespace.into()), - inference_providers: InferenceProviderRegistry::default(), + worker_inference: WorkerInferenceRegistry::default(), } } - /// Creates a registration context backed by host-owned inference providers. + /// Creates a registration context backed by host-owned worker inference. #[doc(hidden)] - pub fn with_inference_providers( + pub fn with_worker_inference( namespace: Option, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Self { Self { registrations: Vec::new(), namespace, - inference_providers, + worker_inference, } } - /// Resolves an inference provider implementing the required contract. + /// Resolves worker inference implementing the required contract. #[doc(hidden)] - pub fn inference_provider( - &self, - name: &str, - expected_contract: &str, - ) -> Result { - self.inference_providers.resolve(name, expected_contract) + pub fn worker_inference(&self, name: &str, expected_contract: &str) -> Result { + self.worker_inference.resolve(name, expected_contract) } /// Returns the runtime-qualified name for a plugin-local registration. @@ -1340,15 +1336,14 @@ pub fn plugin_config_schema() -> Json { /// is removed before the new configuration is activated. #[doc(hidden)] pub async fn initialize_plugins_exact(config: PluginConfig) -> Result { - initialize_plugins_exact_with_inference_providers(config, InferenceProviderRegistry::default()) - .await + initialize_plugins_exact_with_worker_inference(config, WorkerInferenceRegistry::default()).await } -/// Configures plugin components with host-owned inference providers. +/// Configures plugin components with host-owned worker inference. #[doc(hidden)] -pub async fn initialize_plugins_exact_with_inference_providers( +pub async fn initialize_plugins_exact_with_worker_inference( config: PluginConfig, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Result { run_owned_plugin_mutation("plugin initialization", move || async move { let lease = LegacyPluginMutationLease::acquire()?; @@ -1356,7 +1351,7 @@ pub async fn initialize_plugins_exact_with_inference_providers( let initialization = tokio::spawn(initialize_plugins_exact_inner( config, Some(Arc::clone(&rollback_failures)), - inference_providers, + worker_inference, )) .await .map_err(|error| { @@ -1473,16 +1468,16 @@ pub(crate) async fn initialize_plugins_exact_for_host( config: PluginConfig, owner_id: u64, rollback_failures: Arc>>, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Result { verify_plugin_host_owner(owner_id)?; - initialize_plugins_exact_inner(config, Some(rollback_failures), inference_providers).await + initialize_plugins_exact_inner(config, Some(rollback_failures), worker_inference).await } async fn initialize_plugins_exact_inner( config: PluginConfig, rollback_failures: Option>>>, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Result { let enabled_component_count = config .components @@ -1519,7 +1514,7 @@ async fn initialize_plugins_exact_inner( match initialize_plugin_components_catching_panics( config.clone(), rollback_failures.clone(), - inference_providers.clone(), + worker_inference.clone(), ) .await { @@ -1528,7 +1523,7 @@ async fn initialize_plugins_exact_inner( config, report.clone(), registrations, - inference_providers, + worker_inference, )?; log::info!( target: "nemo_relay.plugin", @@ -1541,7 +1536,7 @@ async fn initialize_plugins_exact_inner( Err(err) => match initialize_plugin_components_catching_panics( previous_state.config.clone(), rollback_failures.clone(), - previous_state.inference_providers.clone(), + previous_state.worker_inference.clone(), ) .await { @@ -1551,7 +1546,7 @@ async fn initialize_plugins_exact_inner( previous_state.config, previous_report, registrations, - previous_state.inference_providers, + previous_state.worker_inference, )?; log::warn!( target: "nemo_relay.plugin", @@ -1578,15 +1573,10 @@ async fn initialize_plugins_exact_inner( let registrations = initialize_plugin_components_catching_panics( config.clone(), rollback_failures, - inference_providers.clone(), + worker_inference.clone(), ) .await?; - store_active_plugin_configuration( - config, - report.clone(), - registrations, - inference_providers, - )?; + store_active_plugin_configuration(config, report.clone(), registrations, worker_inference)?; log::info!( target: "nemo_relay.plugin", event = "plugin_configuration_activated", @@ -1600,10 +1590,10 @@ async fn initialize_plugins_exact_inner( async fn initialize_plugin_components_catching_panics( config: PluginConfig, rollback_failures: Option>>>, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Result> { tokio::spawn(async move { - initialize_plugin_components(&config, rollback_failures, inference_providers).await + initialize_plugin_components(&config, rollback_failures, worker_inference).await }) .await .map_err(|error| { @@ -1623,14 +1613,14 @@ pub async fn initialize_plugins(config: PluginConfig) -> Result { initialize_plugins_exact(config).await } -/// Resolves discovered configuration and activates it with host inference providers. +/// Resolves discovered configuration and activates it with host-owned worker inference. #[doc(hidden)] -pub async fn initialize_plugins_with_inference_providers( +pub async fn initialize_plugins_with_worker_inference( config: PluginConfig, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Result { let config = resolve_plugin_config(config)?; - initialize_plugins_exact_with_inference_providers(config, inference_providers).await + initialize_plugins_exact_with_worker_inference(config, worker_inference).await } /// Layers `config` over the default discovered `plugins.toml` files. @@ -2029,13 +2019,13 @@ struct ActivePluginConfiguration { config: PluginConfig, report: ConfigReport, registrations: Vec, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, } async fn initialize_plugin_components( config: &PluginConfig, rollback_failures: Option>>>, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Result> { ensure_builtin_plugins_registered()?; let totals = plugin_component_totals(config); @@ -2067,7 +2057,7 @@ async fn initialize_plugin_components( let mut pending = PendingPluginRegistrationContext::new( namespace, rollback_failures.clone(), - inference_providers.clone(), + worker_inference.clone(), ); plugin .register(&component.config, &mut pending.context) @@ -2116,12 +2106,12 @@ impl PendingPluginRegistrationContext { fn new( namespace: String, rollback_failures: Option>>>, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Self { Self { - context: PluginRegistrationContext::with_inference_providers( + context: PluginRegistrationContext::with_worker_inference( Some(namespace), - inference_providers, + worker_inference, ), rollback_failures, } @@ -2157,7 +2147,7 @@ fn store_active_plugin_configuration( config: PluginConfig, report: ConfigReport, registrations: Vec, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> Result<()> { let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| { PluginError::Internal(format!("active plugin configuration lock poisoned: {err}")) @@ -2166,7 +2156,7 @@ fn store_active_plugin_configuration( config, report, registrations, - inference_providers, + worker_inference, }); Ok(()) } diff --git a/crates/core/src/plugin/dynamic/host.rs b/crates/core/src/plugin/dynamic/host.rs index fe55a803d..07f3c5b3d 100644 --- a/crates/core/src/plugin/dynamic/host.rs +++ b/crates/core/src/plugin/dynamic/host.rs @@ -16,8 +16,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as Json}; use crate::plugin::{ - ConfigReport, InferenceProviderRegistry, PluginComponentSpec, PluginConfig, PluginHostLease, - Result, acquire_plugin_host_lease, clear_plugin_configuration_for_host, + ConfigReport, PluginComponentSpec, PluginConfig, PluginHostLease, Result, + WorkerInferenceRegistry, acquire_plugin_host_lease, clear_plugin_configuration_for_host, ensure_builtin_plugins_registered, initialize_plugins_exact_for_host, resolve_plugin_config, run_owned_plugin_mutation, }; @@ -187,17 +187,17 @@ impl PluginHostActivation { let rollback_failures = Arc::new(Mutex::new(Vec::new())); let owner_id = claim.owner_id(); #[cfg(feature = "worker-grpc")] - let inference_providers = worker + let worker_inference = worker .as_ref() - .map(WorkerPluginActivation::inference_providers) - .unwrap_or_else(InferenceProviderRegistry::default); + .map(WorkerPluginActivation::worker_inference_registry) + .unwrap_or_else(WorkerInferenceRegistry::default); #[cfg(not(feature = "worker-grpc"))] - let inference_providers = InferenceProviderRegistry::default(); + let worker_inference = WorkerInferenceRegistry::default(); let initialization = tokio::spawn(initialize_plugins_exact_for_host( config, owner_id, Arc::clone(&rollback_failures), - inference_providers, + worker_inference, )) .await .map_err(|error| { @@ -270,14 +270,14 @@ impl PluginHostActivation { self.active } - /// Returns the inference providers owned by this activation. + /// Returns the worker inference registry owned by this activation. #[doc(hidden)] - pub fn inference_providers(&self) -> InferenceProviderRegistry { + pub fn worker_inference_registry(&self) -> WorkerInferenceRegistry { #[cfg(feature = "worker-grpc")] if let Some(worker) = &self.worker { - return worker.inference_providers(); + return worker.worker_inference_registry(); } - InferenceProviderRegistry::default() + WorkerInferenceRegistry::default() } /// Clear registered callbacks before unloading libraries and workers. @@ -318,7 +318,7 @@ impl PluginHostActivation { #[cfg(feature = "worker-grpc")] if let Some(worker) = &mut self.worker { runtime_outcome.merge(worker.deregister_plugin_kinds_checked()); - runtime_outcome.merge(worker.deregister_inference_providers_checked()); + runtime_outcome.merge(worker.deregister_worker_inference_checked()); } // A worker cannot be stopped while its registry adapter might still be diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 5bb5e2cbc..62273f8df 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -72,9 +72,9 @@ use crate::codec::request::{ANNOTATED_LLM_REQUEST_SCHEMA, AnnotatedLlmRequest}; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::error::{FlowError, Result as FlowResult}; use crate::plugin::{ - ConfigDiagnostic, DiagnosticLevel, InferenceProviderDescriptor, InferenceProviderRegistration, - InferenceProviderRegistry, Plugin, PluginDeregistrationOutcome, PluginError, - PluginRegistrationContext, deregister_plugin_registration_checked, register_plugin_tracked, + ConfigDiagnostic, DiagnosticLevel, Plugin, PluginDeregistrationOutcome, PluginError, + PluginRegistrationContext, WorkerInferenceDescriptor, WorkerInferenceRegistration, + WorkerInferenceRegistry, deregister_plugin_registration_checked, register_plugin_tracked, }; use super::{ @@ -130,8 +130,8 @@ pub struct WorkerPluginLoadSpec { pub struct WorkerPluginActivation { plugins: Vec>, plugin_registrations: Vec<(String, u64)>, - inference_providers: InferenceProviderRegistry, - inference_provider_registrations: Vec, + worker_inference: WorkerInferenceRegistry, + worker_inference_registrations: Vec, } impl WorkerPluginActivation { @@ -143,20 +143,18 @@ impl WorkerPluginActivation { /// Consumes the activation; deregistration runs from `Drop`. pub fn clear(self) {} - /// Returns the host-owned inference providers installed by this activation. + /// Returns the host-owned worker inference registry installed by this activation. #[doc(hidden)] - pub fn inference_providers(&self) -> InferenceProviderRegistry { - self.inference_providers.clone() + pub fn worker_inference_registry(&self) -> WorkerInferenceRegistry { + self.worker_inference.clone() } pub(crate) fn deregister_plugin_kinds_checked(&mut self) -> DynamicPluginTeardownOutcome { deregister_tracked_registrations_checked(&mut self.plugin_registrations, "worker") } - pub(crate) fn deregister_inference_providers_checked( - &mut self, - ) -> DynamicPluginTeardownOutcome { - deregister_inference_providers_checked(&mut self.inference_provider_registrations) + pub(crate) fn deregister_worker_inference_checked(&mut self) -> DynamicPluginTeardownOutcome { + deregister_worker_inference_checked(&mut self.worker_inference_registrations) } pub(crate) fn shutdown_plugins_checked(&self) -> DynamicPluginTeardownOutcome { @@ -170,7 +168,7 @@ impl WorkerPluginActivation { impl Drop for WorkerPluginActivation { fn drop(&mut self) { - let _ = deregister_inference_providers_checked(&mut self.inference_provider_registrations); + let _ = deregister_worker_inference_checked(&mut self.worker_inference_registrations); for (plugin_kind, registration_id) in self.plugin_registrations.iter().rev() { let _ = deregister_plugin_registration_checked(plugin_kind, *registration_id); } @@ -185,14 +183,14 @@ pub fn load_worker_plugins(specs: I) -> crate::plugin::Result, { - load_worker_plugins_with_inference_providers(specs, InferenceProviderRegistry::default()) + load_worker_plugins_with_worker_inference(specs, WorkerInferenceRegistry::default()) } -/// Loads worker plugins into an existing host inference-provider registry. +/// Loads worker plugins into an existing host-owned worker inference registry. #[doc(hidden)] -pub fn load_worker_plugins_with_inference_providers( +pub fn load_worker_plugins_with_worker_inference( specs: I, - inference_providers: InferenceProviderRegistry, + worker_inference: WorkerInferenceRegistry, ) -> crate::plugin::Result where I: IntoIterator, @@ -200,20 +198,20 @@ where let mut activation = WorkerPluginActivation { plugins: Vec::new(), plugin_registrations: Vec::new(), - inference_providers: inference_providers.clone(), - inference_provider_registrations: Vec::new(), + worker_inference: worker_inference.clone(), + worker_inference_registrations: Vec::new(), }; for spec in specs { let instance = load_one_worker_plugin(&spec)?; - let inference_provider_registrations = - instance.install_inference_providers(&inference_providers)?; + let worker_inference_registrations = + instance.install_worker_inference(&worker_inference)?; let plugin_kind = instance.plugin_kind.clone(); // Transfer ownership before the next fallible registration so Drop can - // unwind providers and the worker process on partial activation. + // unwind worker inference and the worker process on partial activation. activation.plugins.push(instance.clone()); activation - .inference_provider_registrations - .extend(inference_provider_registrations); + .worker_inference_registrations + .extend(worker_inference_registrations); let registration_id = register_plugin_tracked(Arc::new(WorkerPluginAdapter { plugin_kind: plugin_kind.clone(), allows_multiple_components: instance.allows_multiple_components, @@ -1080,10 +1078,10 @@ fn clear_host_python_environment(command: &mut Command) { } impl WorkerPluginInstance { - fn install_inference_providers( + fn install_worker_inference( &self, - registry: &InferenceProviderRegistry, - ) -> crate::plugin::Result> { + registry: &WorkerInferenceRegistry, + ) -> crate::plugin::Result> { let mut registrations = Vec::new(); for registration in &self.registrations { let surface = RegistrationSurface::try_from(registration.surface).map_err(|_| { @@ -1092,23 +1090,23 @@ impl WorkerPluginInstance { self.plugin_kind, registration.surface )) })?; - if surface != RegistrationSurface::InferenceProvider { + if surface != RegistrationSurface::WorkerInference { continue; } let callback_name = registration.local_name.clone(); - let provider_name = format!("{}/{}", self.plugin_kind, callback_name); + let inference_name = format!("{}/{}", self.plugin_kind, callback_name); let callback = self.clone_for_callback(); let descriptor = - InferenceProviderDescriptor::new(provider_name, registration.contract.clone())?; + WorkerInferenceDescriptor::new(inference_name, registration.contract.clone())?; match registry.register( descriptor, Arc::new(move |request, timeout| { - callback.invoke_inference_provider(&callback_name, request, timeout) + callback.invoke_worker_inference(&callback_name, request, timeout) }), ) { Ok(registration) => registrations.push(registration), Err(error) => { - let _ = deregister_inference_providers_checked(&mut registrations); + let _ = deregister_worker_inference_checked(&mut registrations); return Err(error); } } @@ -1155,8 +1153,8 @@ impl WorkerPluginInstance { | RegistrationSurface::LlmStreamExecutionIntercept => { self.install_llm_registration(ctx, registration, surface)? } - RegistrationSurface::InferenceProvider => { - // Providers are installed during host bootstrap so components + RegistrationSurface::WorkerInference => { + // Worker inference is installed during host bootstrap so components // can resolve their contracts before runtime callbacks register. } RegistrationSurface::Unspecified => { @@ -1446,7 +1444,7 @@ struct WorkerPluginCallback { } impl WorkerPluginCallback { - fn invoke_inference_provider( + fn invoke_worker_inference( &self, registration_name: &str, value: Json, @@ -1454,9 +1452,9 @@ impl WorkerPluginCallback { ) -> crate::plugin::Result { let request = self.base_request( registration_name, - RegistrationSurface::InferenceProvider, + RegistrationSurface::WorkerInference, None, - Some(invoke_request_payload::Payload::Provider( + Some(invoke_request_payload::Payload::WorkerInference( json_envelope_infallible(JSON_SCHEMA, &value), )), ); @@ -1466,12 +1464,12 @@ impl WorkerPluginCallback { ) .map_err(|error| { PluginError::RegistrationFailed(format!( - "inference provider '{registration_name}' invocation failed: {error}" + "worker inference '{registration_name}' invocation failed: {error}" )) })?; json_from_invoke_response(response).map_err(|error| { PluginError::RegistrationFailed(format!( - "inference provider '{registration_name}' returned an invalid response: {error}" + "worker inference '{registration_name}' returned an invalid response: {error}" )) }) } @@ -1488,8 +1486,8 @@ impl WorkerPluginCallback { } } -fn deregister_inference_providers_checked( - registrations: &mut Vec, +fn deregister_worker_inference_checked( + registrations: &mut Vec, ) -> DynamicPluginTeardownOutcome { let mut outcome = DynamicPluginTeardownOutcome::success(); for mut registration in std::mem::take(registrations).into_iter().rev() { @@ -1497,17 +1495,17 @@ fn deregister_inference_providers_checked( match registration.deregister_checked() { Ok(PluginDeregistrationOutcome::Removed) => {} Ok(PluginDeregistrationOutcome::Missing) => outcome.record_error( - format!("inference provider '{name}' was not registered during teardown"), + format!("worker inference '{name}' was not registered during teardown"), true, ), Ok(PluginDeregistrationOutcome::Replaced) => outcome.record_error( format!( - "inference provider '{name}' was replaced during teardown and was left registered" + "worker inference '{name}' was replaced during teardown and was left registered" ), true, ), Err(error) => outcome.record_error( - format!("failed to deregister inference provider '{name}': {error}"), + format!("failed to deregister worker inference '{name}': {error}"), false, ), } @@ -3083,15 +3081,15 @@ fn validate_registration_plan( ))); } let contract = registration.contract.trim(); - if surface == RegistrationSurface::InferenceProvider && contract.is_empty() { + if surface == RegistrationSurface::WorkerInference && contract.is_empty() { return Err(PluginError::RegistrationFailed(format!( - "worker plugin '{plugin_id}' returned inference provider '{}' without a contract", + "worker plugin '{plugin_id}' returned worker inference '{}' without a contract", registration.local_name ))); } - if surface != RegistrationSurface::InferenceProvider && !contract.is_empty() { + if surface != RegistrationSurface::WorkerInference && !contract.is_empty() { return Err(PluginError::RegistrationFailed(format!( - "worker plugin '{plugin_id}' returned a contract for non-provider registration '{}'", + "worker plugin '{plugin_id}' returned a contract for non-inference registration '{}'", registration.local_name ))); } diff --git a/crates/core/src/plugin/inference.rs b/crates/core/src/plugin/inference.rs deleted file mode 100644 index fa0b6a43b..000000000 --- a/crates/core/src/plugin/inference.rs +++ /dev/null @@ -1,219 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Host-owned inference-provider services used by plugin components. - -use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -use serde_json::Value as Json; - -use super::{PluginDeregistrationOutcome, PluginError, Result}; - -/// Versioned JSON request-response callback implemented by an inference provider. -#[doc(hidden)] -pub type InferenceProviderFn = Arc Result + Send + Sync + 'static>; - -/// Stable identity and request-response contract for one inference provider. -#[doc(hidden)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InferenceProviderDescriptor { - name: String, - contract: String, -} - -impl InferenceProviderDescriptor { - /// Creates a provider descriptor after validating its stable identifiers. - pub fn new(name: impl Into, contract: impl Into) -> Result { - let name = normalized_identifier(name.into(), "inference provider name")?; - let contract = normalized_identifier(contract.into(), "inference provider contract")?; - Ok(Self { name, contract }) - } - - /// Returns the host-qualified provider name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the versioned request-response contract identifier. - pub fn contract(&self) -> &str { - &self.contract - } -} - -/// Resolved inference provider whose contract has already been checked. -#[doc(hidden)] -#[derive(Clone)] -pub struct InferenceProvider { - descriptor: InferenceProviderDescriptor, - callback: InferenceProviderFn, -} - -impl InferenceProvider { - /// Returns the provider descriptor. - pub fn descriptor(&self) -> &InferenceProviderDescriptor { - &self.descriptor - } - - /// Invokes the provider with the component-owned request and deadline. - pub fn invoke(&self, request: Json, timeout: Duration) -> Result { - (self.callback)(request, timeout) - } -} - -struct RegisteredInferenceProvider { - registration_id: u64, - descriptor: InferenceProviderDescriptor, - callback: InferenceProviderFn, -} - -struct InferenceProviderRegistryInner { - providers: RwLock>, - next_registration_id: AtomicU64, -} - -/// Host-scoped registry for versioned inference providers. -#[doc(hidden)] -#[derive(Clone)] -pub struct InferenceProviderRegistry { - inner: Arc, -} - -impl Default for InferenceProviderRegistry { - fn default() -> Self { - Self { - inner: Arc::new(InferenceProviderRegistryInner { - providers: RwLock::new(HashMap::new()), - next_registration_id: AtomicU64::new(1), - }), - } - } -} - -impl InferenceProviderRegistry { - /// Registers a provider and returns an ownership handle. - pub fn register( - &self, - descriptor: InferenceProviderDescriptor, - callback: InferenceProviderFn, - ) -> Result { - let mut providers = self.inner.providers.write().map_err(|error| { - PluginError::Internal(format!( - "inference provider registry lock poisoned: {error}" - )) - })?; - if providers.contains_key(descriptor.name()) { - return Err(PluginError::RegistrationFailed(format!( - "inference provider '{}' is already registered", - descriptor.name() - ))); - } - let registration_id = self - .inner - .next_registration_id - .fetch_add(1, Ordering::Relaxed); - let name = descriptor.name().to_string(); - providers.insert( - name.clone(), - RegisteredInferenceProvider { - registration_id, - descriptor, - callback, - }, - ); - Ok(InferenceProviderRegistration { - registry: self.clone(), - name, - registration_id: Some(registration_id), - }) - } - - /// Resolves a provider only when its declared contract exactly matches. - pub fn resolve(&self, name: &str, expected_contract: &str) -> Result { - let name = normalized_identifier(name.to_string(), "inference provider name")?; - let expected_contract = - normalized_identifier(expected_contract.to_string(), "inference provider contract")?; - let providers = self.inner.providers.read().map_err(|error| { - PluginError::Internal(format!( - "inference provider registry lock poisoned: {error}" - )) - })?; - let provider = providers.get(&name).ok_or_else(|| { - PluginError::NotFound(format!("inference provider '{name}' is not registered")) - })?; - if provider.descriptor.contract() != expected_contract { - return Err(PluginError::RegistrationFailed(format!( - "inference provider '{name}' implements contract '{}' but '{}' is required", - provider.descriptor.contract(), - expected_contract - ))); - } - Ok(InferenceProvider { - descriptor: provider.descriptor.clone(), - callback: Arc::clone(&provider.callback), - }) - } - - fn deregister(&self, name: &str, registration_id: u64) -> Result { - let mut providers = self.inner.providers.write().map_err(|error| { - PluginError::Internal(format!( - "inference provider registry lock poisoned: {error}" - )) - })?; - match providers.get(name) { - Some(provider) if provider.registration_id == registration_id => { - providers.remove(name); - Ok(PluginDeregistrationOutcome::Removed) - } - Some(_) => Ok(PluginDeregistrationOutcome::Replaced), - None => Ok(PluginDeregistrationOutcome::Missing), - } - } -} - -/// Owned registration for one provider in a host registry. -#[doc(hidden)] -pub struct InferenceProviderRegistration { - registry: InferenceProviderRegistry, - name: String, - registration_id: Option, -} - -impl InferenceProviderRegistration { - /// Returns the registered provider name. - pub fn name(&self) -> &str { - &self.name - } - - pub(crate) fn deregister_checked(&mut self) -> Result { - let Some(registration_id) = self.registration_id.take() else { - return Ok(PluginDeregistrationOutcome::Missing); - }; - self.registry.deregister(&self.name, registration_id) - } -} - -impl Drop for InferenceProviderRegistration { - fn drop(&mut self) { - if let Err(error) = self.deregister_checked() { - log::error!( - target: "nemo_relay.plugin", - event = "inference_provider_cleanup_failed", - provider = self.name.as_str(); - "Inference provider cleanup failed during drop: {error}" - ); - } - } -} - -fn normalized_identifier(value: String, field: &str) -> Result { - let normalized = value.trim(); - if normalized.is_empty() { - return Err(PluginError::RegistrationFailed(format!( - "{field} must not be empty" - ))); - } - Ok(normalized.to_string()) -} diff --git a/crates/core/src/plugin/worker_inference.rs b/crates/core/src/plugin/worker_inference.rs new file mode 100644 index 000000000..85d893cc0 --- /dev/null +++ b/crates/core/src/plugin/worker_inference.rs @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Host-owned worker inference used by plugin components. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; + +use serde_json::Value as Json; + +use super::{PluginDeregistrationOutcome, PluginError, Result}; + +/// Versioned JSON request-response callback implemented by a worker. +#[doc(hidden)] +pub type WorkerInferenceFn = Arc Result + Send + Sync + 'static>; + +/// Stable identity and request-response contract for one worker inference callback. +#[doc(hidden)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WorkerInferenceDescriptor { + name: String, + contract: String, +} + +impl WorkerInferenceDescriptor { + /// Creates a descriptor after validating its stable identifiers. + pub fn new(name: impl Into, contract: impl Into) -> Result { + let name = normalized_identifier(name.into(), "worker inference name")?; + let contract = normalized_identifier(contract.into(), "worker inference contract")?; + Ok(Self { name, contract }) + } + + /// Returns the host-qualified worker inference name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the versioned request-response contract identifier. + pub fn contract(&self) -> &str { + &self.contract + } +} + +/// Resolved worker inference callback whose contract has already been checked. +#[doc(hidden)] +#[derive(Clone)] +pub struct WorkerInference { + descriptor: WorkerInferenceDescriptor, + callback: WorkerInferenceFn, +} + +impl WorkerInference { + /// Returns the worker inference descriptor. + pub fn descriptor(&self) -> &WorkerInferenceDescriptor { + &self.descriptor + } + + /// Invokes the worker with the component-owned request and deadline. + pub fn invoke(&self, request: Json, timeout: Duration) -> Result { + (self.callback)(request, timeout) + } +} + +struct RegisteredWorkerInference { + registration_id: u64, + descriptor: WorkerInferenceDescriptor, + callback: WorkerInferenceFn, +} + +struct WorkerInferenceRegistryInner { + entries: RwLock>, + next_registration_id: AtomicU64, +} + +/// Host-scoped registry for versioned worker inference callbacks. +#[doc(hidden)] +#[derive(Clone)] +pub struct WorkerInferenceRegistry { + inner: Arc, +} + +impl Default for WorkerInferenceRegistry { + fn default() -> Self { + Self { + inner: Arc::new(WorkerInferenceRegistryInner { + entries: RwLock::new(HashMap::new()), + next_registration_id: AtomicU64::new(1), + }), + } + } +} + +impl WorkerInferenceRegistry { + /// Registers worker inference and returns an ownership handle. + pub fn register( + &self, + descriptor: WorkerInferenceDescriptor, + callback: WorkerInferenceFn, + ) -> Result { + let mut entries = self.inner.entries.write().map_err(|error| { + PluginError::Internal(format!("worker inference registry lock poisoned: {error}")) + })?; + if entries.contains_key(descriptor.name()) { + return Err(PluginError::RegistrationFailed(format!( + "worker inference '{}' is already registered", + descriptor.name() + ))); + } + let registration_id = self + .inner + .next_registration_id + .fetch_add(1, Ordering::Relaxed); + let name = descriptor.name().to_string(); + entries.insert( + name.clone(), + RegisteredWorkerInference { + registration_id, + descriptor, + callback, + }, + ); + Ok(WorkerInferenceRegistration { + registry: self.clone(), + name, + registration_id: Some(registration_id), + }) + } + + /// Resolves worker inference only when its declared contract exactly matches. + pub fn resolve(&self, name: &str, expected_contract: &str) -> Result { + let name = normalized_identifier(name.to_string(), "worker inference name")?; + let expected_contract = + normalized_identifier(expected_contract.to_string(), "worker inference contract")?; + let entries = self.inner.entries.read().map_err(|error| { + PluginError::Internal(format!("worker inference registry lock poisoned: {error}")) + })?; + let entry = entries.get(&name).ok_or_else(|| { + PluginError::NotFound(format!("worker inference '{name}' is not registered")) + })?; + if entry.descriptor.contract() != expected_contract { + return Err(PluginError::RegistrationFailed(format!( + "worker inference '{name}' implements contract '{}' but '{}' is required", + entry.descriptor.contract(), + expected_contract + ))); + } + Ok(WorkerInference { + descriptor: entry.descriptor.clone(), + callback: Arc::clone(&entry.callback), + }) + } + + fn deregister(&self, name: &str, registration_id: u64) -> Result { + let mut entries = self.inner.entries.write().map_err(|error| { + PluginError::Internal(format!("worker inference registry lock poisoned: {error}")) + })?; + match entries.get(name) { + Some(entry) if entry.registration_id == registration_id => { + entries.remove(name); + Ok(PluginDeregistrationOutcome::Removed) + } + Some(_) => Ok(PluginDeregistrationOutcome::Replaced), + None => Ok(PluginDeregistrationOutcome::Missing), + } + } +} + +/// Owned registration for one worker inference callback in a host registry. +#[doc(hidden)] +pub struct WorkerInferenceRegistration { + registry: WorkerInferenceRegistry, + name: String, + registration_id: Option, +} + +impl WorkerInferenceRegistration { + /// Returns the registered worker inference name. + pub fn name(&self) -> &str { + &self.name + } + + pub(crate) fn deregister_checked(&mut self) -> Result { + let Some(registration_id) = self.registration_id.take() else { + return Ok(PluginDeregistrationOutcome::Missing); + }; + self.registry.deregister(&self.name, registration_id) + } +} + +impl Drop for WorkerInferenceRegistration { + fn drop(&mut self) { + if let Err(error) = self.deregister_checked() { + log::error!( + target: "nemo_relay.plugin", + event = "worker_inference_cleanup_failed", + worker_inference = self.name.as_str(); + "Worker inference cleanup failed during drop: {error}" + ); + } + } +} + +fn normalized_identifier(value: String, field: &str) -> Result { + let normalized = value.trim(); + if normalized.is_empty() { + return Err(PluginError::RegistrationFailed(format!( + "{field} must not be empty" + ))); + } + Ok(normalized.to_string()) +} diff --git a/crates/core/tests/fixtures/worker_plugin/src/main.rs b/crates/core/tests/fixtures/worker_plugin/src/main.rs index 99b3254ab..8ad3b6755 100644 --- a/crates/core/tests/fixtures/worker_plugin/src/main.rs +++ b/crates/core/tests/fixtures/worker_plugin/src/main.rs @@ -59,8 +59,8 @@ impl WorkerPlugin for FixtureWorkerPlugin { ctx.register_subscriber("", |_| {}); return Ok(()); } - let inference_provider_names = config - .get("inference_provider_names") + let worker_inference_names = config + .get("worker_inference_names") .and_then(Json::as_array) .map(|names| { names @@ -72,21 +72,21 @@ impl WorkerPlugin for FixtureWorkerPlugin { .unwrap_or_else(|| { vec![ config - .get("inference_provider_name") + .get("worker_inference_name") .and_then(Json::as_str) .unwrap_or("fixture_local_model") .to_string(), ] }); - for provider_name in inference_provider_names { - let callback_provider_name = provider_name.clone(); + for inference_name in worker_inference_names { + let callback_inference_name = inference_name.clone(); let exit_in_local_model = fixture_flag(config, "exit_in_local_model"); let contract = config - .get("inference_provider_contract") + .get("worker_inference_contract") .and_then(Json::as_str) .unwrap_or(DEFAULT_INFERENCE_CONTRACT); - ctx.register_inference_provider(&provider_name, contract, move |request| { - let provider_name = callback_provider_name.clone(); + ctx.register_worker_inference(&inference_name, contract, move |request| { + let inference_name = callback_inference_name.clone(); async move { if exit_in_local_model { std::process::exit(45); @@ -115,12 +115,12 @@ impl WorkerPlugin for FixtureWorkerPlugin { Ok(json!({ "version": 1, "request": request, - "provider": provider_name + "worker_inference": inference_name })) } }); } - if fixture_flag(config, "provider_only") { + if fixture_flag(config, "worker_inference_only") { return Ok(()); } diff --git a/crates/core/tests/integration/worker_plugin_tests.rs b/crates/core/tests/integration/worker_plugin_tests.rs index b83940da2..692b9e943 100644 --- a/crates/core/tests/integration/worker_plugin_tests.rs +++ b/crates/core/tests/integration/worker_plugin_tests.rs @@ -24,12 +24,12 @@ use nemo_relay::codec::traits::LlmCodec; use nemo_relay::error::Result as FlowResult; use nemo_relay::plugin::dynamic::{ DynamicPluginActivationSpec, DynamicPluginKind, PluginHostActivation, WorkerPluginActivation, - WorkerPluginLoadSpec, load_worker_plugins, load_worker_plugins_with_inference_providers, + WorkerPluginLoadSpec, load_worker_plugins, load_worker_plugins_with_worker_inference, }; use nemo_relay::plugin::{ - InferenceProviderDescriptor, InferenceProviderRegistry, PluginComponentSpec, PluginConfig, + PluginComponentSpec, PluginConfig, WorkerInferenceDescriptor, WorkerInferenceRegistry, clear_plugin_configuration, initialize_plugins_exact, - initialize_plugins_exact_with_inference_providers, list_plugin_kinds, + initialize_plugins_exact_with_worker_inference, list_plugin_kinds, }; use serde_json::{Map, Value as Json, json}; use sha2::{Digest, Sha256}; @@ -37,7 +37,6 @@ use tempfile::TempDir; use uuid::Uuid; const PII_DETECTION_CONTRACT: &str = "nemo.relay.pii_detection.v1"; -const EXAMPLE_ECHO_CONTRACT: &str = "examples.python_grpc_worker.echo.v1"; static WORKER_PLUGIN_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); fn enable_operational_logs() { @@ -54,7 +53,7 @@ fn worker_activation_with_no_specs_is_empty() { } #[tokio::test(flavor = "multi_thread")] -async fn worker_inference_provider_is_preinstalled_times_out_and_clears() { +async fn worker_inference_is_preinstalled_times_out_and_clears() { let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; let fixture = build_fixture_worker(); let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); @@ -65,31 +64,31 @@ async fn worker_inference_provider_is_preinstalled_times_out_and_clears() { config: Map::new(), }]) .expect("worker plugin should load"); - let registry = activation.inference_providers(); + let registry = activation.worker_inference_registry(); - // Providers must be available before static consumers initialize. - let provider = registry + // Worker inference must be available before static consumers initialize. + let inference = registry .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) - .expect("worker provider should be installed"); + .expect("worker inference should be installed"); assert_eq!( - provider + inference .invoke( json!({"text": "private"}), std::time::Duration::from_secs(1) ) - .expect("worker provider should return JSON"), + .expect("worker inference should return JSON"), json!({ "version": 1, "request": {"text": "private"}, - "provider": "fixture_local_model" + "worker_inference": "fixture_local_model" }) ); - let timeout = provider + let timeout = inference .invoke( json!({"delay_ms": 100}), std::time::Duration::from_millis(5), ) - .expect_err("worker provider should honor the caller deadline") + .expect_err("worker inference should honor the caller deadline") .to_string(); assert!(timeout.contains("timed out"), "{timeout}"); @@ -98,7 +97,7 @@ async fn worker_inference_provider_is_preinstalled_times_out_and_clears() { registry .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) .is_err(), - "provider should be removed when the worker activation clears" + "inference should be removed when the worker activation clears" ); } @@ -114,12 +113,12 @@ async fn worker_clear_fails_an_in_flight_local_model_call_without_hanging() { config: Map::new(), }]) .expect("worker plugin should load"); - let registry = activation.inference_providers(); - let provider = registry + let registry = activation.worker_inference_registry(); + let inference = registry .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) - .expect("worker provider should be installed"); + .expect("worker inference should be installed"); let invocation = std::thread::spawn(move || { - provider.invoke( + inference.invoke( json!({"delay_ms": 5_000}), std::time::Duration::from_secs(10), ) @@ -130,7 +129,7 @@ async fn worker_clear_fails_an_in_flight_local_model_call_without_hanging() { let error = invocation .join() - .expect("provider invocation thread should join") + .expect("inference invocation thread should join") .expect_err("clearing the worker must fail its in-flight call") .to_string(); assert!( @@ -141,88 +140,88 @@ async fn worker_clear_fails_an_in_flight_local_model_call_without_hanging() { registry .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) .is_err(), - "provider should remain deregistered after concurrent clear" + "inference should remain deregistered after concurrent clear" ); } #[tokio::test(flavor = "multi_thread")] -async fn worker_provider_rolls_back_after_later_plugin_registration_failure() { +async fn worker_inference_rolls_back_after_later_plugin_registration_failure() { let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; let fixture = build_fixture_worker(); let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); - let first_provider = "fixture_local_model_first"; - let first_provider_key = format!("fixture_worker/{first_provider}"); - let registry = InferenceProviderRegistry::default(); - let first = load_worker_plugins_with_inference_providers( + let first_inference = "fixture_local_model_first"; + let first_inference_key = format!("fixture_worker/{first_inference}"); + let registry = WorkerInferenceRegistry::default(); + let first = load_worker_plugins_with_worker_inference( [WorkerPluginLoadSpec { plugin_id: "fixture_worker".into(), manifest_ref: manifest_ref.to_string_lossy().into_owned(), environment_ref: None, - config: Map::from_iter([("inference_provider_name".into(), json!(first_provider))]), + config: Map::from_iter([("worker_inference_name".into(), json!(first_inference))]), }], registry.clone(), ) .expect("first worker plugin should load"); - let second_provider = "fixture_local_model_rollback"; - let second_provider_key = format!("fixture_worker/{second_provider}"); - let second = load_worker_plugins_with_inference_providers( + let second_inference = "fixture_local_model_rollback"; + let second_inference_key = format!("fixture_worker/{second_inference}"); + let second = load_worker_plugins_with_worker_inference( [WorkerPluginLoadSpec { plugin_id: "fixture_worker".into(), manifest_ref: manifest_ref.to_string_lossy().into_owned(), environment_ref: None, - config: Map::from_iter([("inference_provider_name".into(), json!(second_provider))]), + config: Map::from_iter([("worker_inference_name".into(), json!(second_inference))]), }], registry.clone(), ); assert!( second.is_err(), - "duplicate plugin kind should fail after the second provider is installed" + "duplicate plugin kind should fail after the second inference is installed" ); assert!( registry - .resolve(&second_provider_key, PII_DETECTION_CONTRACT) + .resolve(&second_inference_key, PII_DETECTION_CONTRACT) .is_err(), - "the second provider must be rolled back with its failed activation" + "the second inference must be rolled back with its failed activation" ); assert!( registry - .resolve(&first_provider_key, PII_DETECTION_CONTRACT) + .resolve(&first_inference_key, PII_DETECTION_CONTRACT) .is_ok(), - "rollback must not remove the first activation's provider" + "rollback must not remove the first activation's inference" ); first.clear(); assert!( registry - .resolve(&first_provider_key, PII_DETECTION_CONTRACT) + .resolve(&first_inference_key, PII_DETECTION_CONTRACT) .is_err() ); } #[tokio::test(flavor = "multi_thread")] -async fn worker_provider_rolls_back_earlier_provider_after_same_worker_conflict() { +async fn worker_inference_rolls_back_earlier_inference_after_same_worker_conflict() { let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; let fixture = build_fixture_worker(); let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); - let first_provider_key = "fixture_worker/fixture_local_model_unique"; - let conflicting_provider_key = "fixture_worker/fixture_local_model_conflict"; - let registry = InferenceProviderRegistry::default(); + let first_inference_key = "fixture_worker/fixture_local_model_unique"; + let conflicting_inference_key = "fixture_worker/fixture_local_model_conflict"; + let registry = WorkerInferenceRegistry::default(); let _existing_registration = registry .register( - InferenceProviderDescriptor::new(conflicting_provider_key, PII_DETECTION_CONTRACT) + WorkerInferenceDescriptor::new(conflicting_inference_key, PII_DETECTION_CONTRACT) .unwrap(), Arc::new(|request, _| Ok(json!({"existing": request}))), ) - .expect("conflicting provider fixture should register"); + .expect("conflicting inference fixture should register"); - let activation = load_worker_plugins_with_inference_providers( + let activation = load_worker_plugins_with_worker_inference( [WorkerPluginLoadSpec { plugin_id: "fixture_worker".into(), manifest_ref: manifest_ref.to_string_lossy().into_owned(), environment_ref: None, config: Map::from_iter([( - "inference_provider_names".into(), + "worker_inference_names".into(), json!(["fixture_local_model_unique", "fixture_local_model_conflict"]), )]), }], @@ -231,21 +230,21 @@ async fn worker_provider_rolls_back_earlier_provider_after_same_worker_conflict( assert!( activation.is_err(), - "the worker activation should fail on its second provider" + "the worker activation should fail on its second inference" ); assert!( registry - .resolve(first_provider_key, PII_DETECTION_CONTRACT) + .resolve(first_inference_key, PII_DETECTION_CONTRACT) .is_err(), - "an earlier provider from the failed worker must be rolled back" + "an earlier inference from the failed worker must be rolled back" ); let existing = registry - .resolve(conflicting_provider_key, PII_DETECTION_CONTRACT) - .expect("the existing conflicting provider must remain registered"); + .resolve(conflicting_inference_key, PII_DETECTION_CONTRACT) + .expect("the existing conflicting inference must remain registered"); assert_eq!( existing .invoke(json!({"value": 1}), std::time::Duration::from_secs(1)) - .expect("existing provider should remain callable"), + .expect("existing inference should remain callable"), json!({"existing": {"value": 1}}) ); } @@ -1297,7 +1296,6 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { config: config.clone(), }]) .expect("managed Python worker should load"); - let inference_providers = activation.inference_providers(); let mut cleanup = PythonWorkerCleanup::new(activation); let mut plugin_config = PluginConfig::default(); @@ -1306,7 +1304,7 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { enabled: true, config, }); - initialize_plugins_exact_with_inference_providers(plugin_config, inference_providers.clone()) + initialize_plugins_exact(plugin_config) .await .expect("managed Python worker should initialize"); @@ -1326,24 +1324,6 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { rewritten["_nemo_relay_plugin"]["tag"], "managed-environment" ); - let inference_provider = inference_providers - .resolve("examples.python_grpc_worker/echo", EXAMPLE_ECHO_CONTRACT) - .expect("Python worker should expose its inference provider"); - assert_eq!( - inference_provider - .invoke( - json!({"version": 1, "texts": [{"id": 0, "text": "private"}]}), - std::time::Duration::from_secs(1), - ) - .expect("Python inference provider should round-trip JSON"), - json!({ - "provider": "python_grpc_worker", - "request": { - "version": 1, - "texts": [{"id": 0, "text": "private"}], - }, - }), - ); flush_subscribers().expect("Python callback mark should flush"); find_event( &events.lock().unwrap(), @@ -1455,7 +1435,7 @@ async fn load_and_initialize_fixture(config: Map) -> LoadedWorker config: config.clone(), }]) .expect("worker plugin should load"); - let inference_providers = activation.inference_providers(); + let worker_inference = activation.worker_inference_registry(); let mut plugin_config = PluginConfig::default(); plugin_config.components.push(PluginComponentSpec { @@ -1463,7 +1443,7 @@ async fn load_and_initialize_fixture(config: Map) -> LoadedWorker enabled: true, config, }); - initialize_plugins_exact_with_inference_providers(plugin_config, inference_providers) + initialize_plugins_exact_with_worker_inference(plugin_config, worker_inference) .await .expect("worker plugin should initialize"); diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index 0d250926d..f3e515cb5 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -298,13 +298,13 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { "fixture_worker", &RegisterResponse { registrations: vec![registration( - RegistrationSurface::InferenceProvider, + RegistrationSurface::WorkerInference, "detector", )], error: None, }, ) - .expect_err("inference providers must declare a contract"); + .expect_err("worker inference must declare a contract"); assert!(missing_contract.to_string().contains("without a contract")); let contract_on_middleware = validate_registration_plan( @@ -317,20 +317,20 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { error: None, }, ) - .expect_err("middleware registrations must not declare provider contracts"); - assert!(contract_on_middleware.to_string().contains("non-provider")); + .expect_err("middleware registrations must not declare inference contracts"); + assert!(contract_on_middleware.to_string().contains("non-inference")); validate_registration_plan( "fixture_worker", &RegisterResponse { registrations: vec![Registration { contract: "test.detector.v1".into(), - ..registration(RegistrationSurface::InferenceProvider, "detector") + ..registration(RegistrationSurface::WorkerInference, "detector") }], error: None, }, ) - .expect("versioned inference provider contract should be accepted"); + .expect("versioned worker inference contract should be accepted"); let cases = [ (ProtoScopeType::Agent, crate::api::scope::ScopeType::Agent), diff --git a/crates/core/tests/unit/plugin_tests.rs b/crates/core/tests/unit/plugin_tests.rs index 2cab29057..9055009cb 100644 --- a/crates/core/tests/unit/plugin_tests.rs +++ b/crates/core/tests/unit/plugin_tests.rs @@ -26,7 +26,7 @@ struct RecordingPlugin; struct ReplacementPlugin; struct RestoreFailPlugin; struct RestoreBreakPlugin; -struct ProviderAwarePlugin; +struct WorkerInferenceAwarePlugin; struct PartialFailPlugin; struct VanishingPlugin; struct BlockingPlugin { @@ -56,14 +56,14 @@ static PARTIAL_FAIL_ROLLBACKS: AtomicUsize = AtomicUsize::new(0); static RESTORE_FAIL_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); static RESTORE_BREAK_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); static REPLACEMENT_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); -static INFERENCE_PROVIDER_VALUES: OnceLock>> = OnceLock::new(); +static WORKER_INFERENCE_VALUES: OnceLock>> = OnceLock::new(); fn recorded_names() -> &'static Mutex> { RECORDED_NAMES.get_or_init(|| Mutex::new(Vec::new())) } -fn inference_provider_values() -> &'static Mutex> { - INFERENCE_PROVIDER_VALUES.get_or_init(|| Mutex::new(Vec::new())) +fn worker_inference_values() -> &'static Mutex> { + WORKER_INFERENCE_VALUES.get_or_init(|| Mutex::new(Vec::new())) } fn lock_runtime_owner() -> std::sync::MutexGuard<'static, ()> { @@ -311,9 +311,9 @@ impl Plugin for RestoreBreakPlugin { } } -impl Plugin for ProviderAwarePlugin { +impl Plugin for WorkerInferenceAwarePlugin { fn plugin_kind(&self) -> &str { - "provider-aware.plugin" + "worker-inference-aware.plugin" } fn validate(&self, _plugin_config: &Map) -> Vec { @@ -326,17 +326,17 @@ impl Plugin for ProviderAwarePlugin { ctx: &'a mut PluginRegistrationContext, ) -> Pin> + Send + 'a>> { Box::pin(async move { - let provider = ctx.inference_provider("shared-provider", "test.echo.v1")?; - let response = provider.invoke(json!({}), std::time::Duration::from_secs(1))?; + let inference = ctx.worker_inference("shared-inference", "test.echo.v1")?; + let response = inference.invoke(json!({}), std::time::Duration::from_secs(1))?; let source = response .get("source") .and_then(Json::as_str) .ok_or_else(|| { PluginError::RegistrationFailed( - "provider-aware.plugin received an invalid response".into(), + "worker-inference-aware.plugin received an invalid response".into(), ) })?; - inference_provider_values() + worker_inference_values() .lock() .unwrap() .push(source.to_string()); @@ -517,14 +517,14 @@ fn reset_global() { RESTORE_FAIL_REGISTRATIONS.store(0, Ordering::SeqCst); RESTORE_BREAK_REGISTRATIONS.store(0, Ordering::SeqCst); REPLACEMENT_REGISTRATIONS.store(0, Ordering::SeqCst); - inference_provider_values().lock().unwrap().clear(); + worker_inference_values().lock().unwrap().clear(); let _ = deregister_plugin("test.plugin"); let _ = deregister_plugin("singleton.plugin"); let _ = deregister_plugin("recording.plugin"); let _ = deregister_plugin("replacement.plugin"); let _ = deregister_plugin("restore.fail.plugin"); let _ = deregister_plugin("restore.break.plugin"); - let _ = deregister_plugin("provider-aware.plugin"); + let _ = deregister_plugin("worker-inference-aware.plugin"); let _ = deregister_plugin("partial.fail.plugin"); let _ = deregister_plugin("vanishing.plugin"); let _ = deregister_plugin("blocking.plugin"); @@ -1153,23 +1153,23 @@ fn test_initialize_plugins_restores_previous_configuration_after_failed_replacem } #[test] -fn test_failed_replacement_restores_previous_inference_provider_registry() { +fn test_failed_replacement_restores_previous_worker_inference_registry() { let _guard = lock_runtime_owner(); reset_global(); - register_plugin(Arc::new(ProviderAwarePlugin)).unwrap(); + register_plugin(Arc::new(WorkerInferenceAwarePlugin)).unwrap(); register_plugin(Arc::new(RestoreFailPlugin)).unwrap(); - let previous_registry = InferenceProviderRegistry::default(); - let _previous_provider = previous_registry + let previous_registry = WorkerInferenceRegistry::default(); + let _previous_inference = previous_registry .register( - InferenceProviderDescriptor::new("shared-provider", "test.echo.v1").unwrap(), + WorkerInferenceDescriptor::new("shared-inference", "test.echo.v1").unwrap(), Arc::new(|_, _| Ok(json!({"source": "previous"}))), ) .unwrap(); - let replacement_registry = InferenceProviderRegistry::default(); - let _replacement_provider = replacement_registry + let replacement_registry = WorkerInferenceRegistry::default(); + let _replacement_inference = replacement_registry .register( - InferenceProviderDescriptor::new("shared-provider", "test.echo.v1").unwrap(), + WorkerInferenceDescriptor::new("shared-inference", "test.echo.v1").unwrap(), Arc::new(|_, _| Ok(json!({"source": "replacement"}))), ) .unwrap(); @@ -1179,9 +1179,9 @@ fn test_failed_replacement_restores_previous_inference_provider_registry() { .build() .unwrap(); runtime - .block_on(initialize_plugins_exact_with_inference_providers( + .block_on(initialize_plugins_exact_with_worker_inference( PluginConfig { - components: vec![PluginComponentSpec::new("provider-aware.plugin")], + components: vec![PluginComponentSpec::new("worker-inference-aware.plugin")], ..PluginConfig::default() }, previous_registry, @@ -1189,7 +1189,7 @@ fn test_failed_replacement_restores_previous_inference_provider_registry() { .unwrap(); let error = runtime - .block_on(initialize_plugins_exact_with_inference_providers( + .block_on(initialize_plugins_exact_with_worker_inference( PluginConfig { components: vec![PluginComponentSpec::new("restore.fail.plugin")], ..PluginConfig::default() @@ -1199,7 +1199,7 @@ fn test_failed_replacement_restores_previous_inference_provider_registry() { .unwrap_err(); assert!(error.to_string().contains("refused to initialize")); assert_eq!( - *inference_provider_values().lock().unwrap(), + *worker_inference_values().lock().unwrap(), vec!["previous", "previous"] ); @@ -1414,7 +1414,7 @@ fn test_checked_teardown_reports_unremoved_registrations() { )) }), )], - InferenceProviderRegistry::default(), + WorkerInferenceRegistry::default(), ) .unwrap(); @@ -1440,7 +1440,7 @@ fn test_legacy_clear_retains_mutation_owner_after_incomplete_teardown() { "stale-callback", Box::new(|| panic!("fixture deregistration panicked")), )], - InferenceProviderRegistry::default(), + WorkerInferenceRegistry::default(), ) .unwrap(); diff --git a/crates/core/tests/unit/inference_provider_tests.rs b/crates/core/tests/unit/worker_inference_tests.rs similarity index 53% rename from crates/core/tests/unit/inference_provider_tests.rs rename to crates/core/tests/unit/worker_inference_tests.rs index 5e9ccc5e7..908dc5f32 100644 --- a/crates/core/tests/unit/inference_provider_tests.rs +++ b/crates/core/tests/unit/worker_inference_tests.rs @@ -6,14 +6,14 @@ use std::time::Duration; use serde_json::json; -use crate::plugin::{InferenceProviderDescriptor, InferenceProviderRegistry}; +use crate::plugin::{WorkerInferenceDescriptor, WorkerInferenceRegistry}; #[test] -fn provider_round_trips_json_and_receives_deadline() { - let registry = InferenceProviderRegistry::default(); +fn inference_round_trips_json_and_receives_deadline() { + let registry = WorkerInferenceRegistry::default(); let _registration = registry .register( - InferenceProviderDescriptor::new("test-provider", "test.echo.v1").unwrap(), + WorkerInferenceDescriptor::new("test-inference", "test.echo.v1").unwrap(), Arc::new(|request, timeout| { assert_eq!(timeout, Duration::from_millis(25)); Ok(json!({"request": request})) @@ -21,9 +21,9 @@ fn provider_round_trips_json_and_receives_deadline() { ) .unwrap(); - let provider = registry.resolve("test-provider", "test.echo.v1").unwrap(); + let inference = registry.resolve("test-inference", "test.echo.v1").unwrap(); assert_eq!( - provider + inference .invoke(json!({"text": "hello"}), Duration::from_millis(25)) .unwrap(), json!({"request": {"text": "hello"}}) @@ -31,64 +31,63 @@ fn provider_round_trips_json_and_receives_deadline() { } #[test] -fn registration_owns_provider_lifetime() { - let registry = InferenceProviderRegistry::default(); +fn registration_owns_inference_lifetime() { + let registry = WorkerInferenceRegistry::default(); let registration = registry .register( - InferenceProviderDescriptor::new("owned-provider", "test.echo.v1").unwrap(), + WorkerInferenceDescriptor::new("owned-inference", "test.echo.v1").unwrap(), Arc::new(|request, _| Ok(request)), ) .unwrap(); - assert!(registry.resolve("owned-provider", "test.echo.v1").is_ok()); + assert!(registry.resolve("owned-inference", "test.echo.v1").is_ok()); drop(registration); - assert!(registry.resolve("owned-provider", "test.echo.v1").is_err()); + assert!(registry.resolve("owned-inference", "test.echo.v1").is_err()); } #[test] -fn duplicate_provider_names_are_rejected() { - let registry = InferenceProviderRegistry::default(); +fn duplicate_inference_names_are_rejected() { + let registry = WorkerInferenceRegistry::default(); let _registration = registry .register( - InferenceProviderDescriptor::new("duplicate-provider", "test.echo.v1").unwrap(), + WorkerInferenceDescriptor::new("duplicate-inference", "test.echo.v1").unwrap(), Arc::new(|request, _| Ok(request)), ) .unwrap(); let duplicate = registry .register( - InferenceProviderDescriptor::new("duplicate-provider", "test.other.v1").unwrap(), + WorkerInferenceDescriptor::new("duplicate-inference", "test.other.v1").unwrap(), Arc::new(|request, _| Ok(request)), ) .err() - .expect("duplicate provider names must fail"); + .expect("duplicate inference names must fail"); assert!(duplicate.to_string().contains("already registered")); } #[test] -fn provider_names_are_normalized_consistently() { - let registry = InferenceProviderRegistry::default(); +fn inference_names_are_normalized_consistently() { + let registry = WorkerInferenceRegistry::default(); let _registration = registry .register( - InferenceProviderDescriptor::new(" normalized-provider ", " test.echo.v1 ") - .unwrap(), + WorkerInferenceDescriptor::new(" normalized-inference ", " test.echo.v1 ").unwrap(), Arc::new(|request, _| Ok(request)), ) .unwrap(); assert!( registry - .resolve(" normalized-provider ", " test.echo.v1 ") + .resolve(" normalized-inference ", " test.echo.v1 ") .is_ok() ); } #[test] -fn provider_contract_mismatch_is_rejected_before_invocation() { - let registry = InferenceProviderRegistry::default(); +fn inference_contract_mismatch_is_rejected_before_invocation() { + let registry = WorkerInferenceRegistry::default(); let _registration = registry .register( - InferenceProviderDescriptor::new("detector", "test.detector.v1").unwrap(), + WorkerInferenceDescriptor::new("detector", "test.detector.v1").unwrap(), Arc::new(|request, _| Ok(request)), ) .unwrap(); @@ -102,12 +101,12 @@ fn provider_contract_mismatch_is_rejected_before_invocation() { } #[test] -fn registries_isolate_provider_names_between_hosts() { - let first = InferenceProviderRegistry::default(); - let second = InferenceProviderRegistry::default(); +fn registries_isolate_inference_names_between_hosts() { + let first = WorkerInferenceRegistry::default(); + let second = WorkerInferenceRegistry::default(); let _first_registration = first .register( - InferenceProviderDescriptor::new("shared-name", "test.echo.v1").unwrap(), + WorkerInferenceDescriptor::new("shared-name", "test.echo.v1").unwrap(), Arc::new(|request, _| Ok(request)), ) .unwrap(); diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index 10f3d4cf6..458f187a7 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -194,7 +194,7 @@ observability fields, decoding provider payloads, batching text, enforcing the deadline and failure policy, validating detections, and replacing accepted spans. -Configure the provider by its host-qualified name: +Configure worker inference by its host-qualified name: ```toml [[components]] @@ -223,9 +223,9 @@ max_latency_ms = 250 The backend name is `/`. For example, a worker with plugin ID `acme.pii_worker` that calls -`register_inference_provider("detector", "nemo.relay.pii_detection.v1", ...)` +`register_worker_inference("detector", "nemo.relay.pii_detection.v1", ...)` is selected as `acme.pii_worker/detector`. Relay verifies the PII contract, -installs worker providers before static components initialize, and removes PII +installs worker inference before static components initialize, and removes PII sanitizers before stopping their worker. Use profiles to compose deterministic and contextual detection. The lower @@ -272,11 +272,11 @@ a classifier unless that is an explicit policy choice. Relay accepts detections whose confidence is at least `min_score`, which defaults to `0.4`. `excluded_labels` is an exact, case-sensitive denylist for -provider labels that should remain visible. The host applies both settings -after validating the complete provider response; workers do not own the final +detection labels that should remain visible. The host applies both settings +after validating the complete worker response; workers do not own the final redaction policy. -Provider failures, timeouts, malformed responses, invalid UTF-8 boundaries, +Worker failures, timeouts, malformed responses, invalid UTF-8 boundaries, overlapping spans, and input-limit violations fail closed for the affected batch. If a configured codec cannot decode or safely re-encode an LLM payload, Relay omits that request or response payload from the emitted event; it does not @@ -284,11 +284,11 @@ retry normalized selectors against the raw provider shape. `allow_network = true` is rejected; this lane is for same-machine inference. This setting is a configuration invariant, not a network sandbox: Relay's worker launcher does not currently prevent a worker process from opening -sockets. Only install providers whose packaging and runtime behavior satisfy +sockets. Only install workers whose packaging and runtime behavior satisfy that policy. The default deadline is 250 ms for the complete selected payload, -including every provider batch. Configuration above 60 seconds is rejected. +including every inference batch. Configuration above 60 seconds is rejected. -### Provider Contract +### PII Detection Contract The worker receives a versioned JSON request: @@ -320,16 +320,16 @@ It returns detections using UTF-8 byte offsets: } ``` -The provider performs inference only. It must not choose Relay surfaces, +The worker performs inference only. It must not choose Relay surfaces, traverse arbitrary event fields, or apply replacements itself. Rust and Python workers have SDK helpers for this registration. Other languages can implement the same `grpc-v1` protobuf contract directly; Rust, Python, and Node hosts all consume it through the shared core runtime. -### Optional Rampart Provider +### Optional Rampart Worker The source tree includes an optional -[Rampart worker](./providers/rampart/README.md) that implements this provider +[Rampart worker](./workers/rampart/README.md) that implements this detection contract with a pinned ONNX token-classification model. It runs in a Relay-managed Python worker process, keeps ONNX dependencies out of the host, and complements the built-in deterministic recognizers. The model is prefetched diff --git a/crates/pii-redaction/src/component.rs b/crates/pii-redaction/src/component.rs index bddd024c2..2217c2b21 100644 --- a/crates/pii-redaction/src/component.rs +++ b/crates/pii-redaction/src/component.rs @@ -28,8 +28,8 @@ use super::local::{register_local_backend, validate_local_backend_config}; /// The plugin kind reserved for the built-in privacy component. pub const PII_REDACTION_PLUGIN_KIND: &str = "pii_redaction"; -/// Versioned inference contract implemented by PII detection providers. -pub const PII_DETECTION_PROVIDER_CONTRACT: &str = "nemo.relay.pii_detection.v1"; +/// Versioned inference contract implemented by PII detection workers. +pub const PII_DETECTION_CONTRACT: &str = "nemo.relay.pii_detection.v1"; pub(super) const DEFAULT_LOCAL_MODEL_LATENCY_MS: u64 = 250; pub(super) const DEFAULT_LOCAL_MODEL_MIN_SCORE: f64 = 0.4; pub(super) const MAX_LOCAL_MODEL_LATENCY_MS: u64 = 60_000; @@ -271,17 +271,17 @@ impl Default for BuiltinBackendConfig { } } -/// Local-backend settings for a same-machine local-model provider. +/// Local-backend settings for same-machine worker inference. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct LocalBackendConfig { - /// Registered local-model provider identifier. + /// Registered worker inference identifier. #[serde(default, skip_serializing_if = "Option::is_none")] pub backend: Option, - /// Optional model identifier passed to the provider. + /// Optional model identifier passed to the worker. #[serde(default, skip_serializing_if = "Option::is_none")] pub model_id: Option, - /// Optional detector profile passed to the provider. + /// Optional detector profile passed to the worker. #[serde(default, skip_serializing_if = "Option::is_none")] pub detector_profile: Option, /// Exact JSON-pointer paths to inspect. Empty means every string leaf. @@ -290,19 +290,19 @@ pub struct LocalBackendConfig { /// JSON-pointer patterns to inspect. A `*` segment matches one path segment. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub target_path_patterns: Vec, - /// Minimum provider confidence accepted for redaction. + /// Minimum detection confidence accepted for redaction. #[serde(default, skip_serializing_if = "Option::is_none")] pub min_score: Option, - /// Provider labels that should not be redacted. + /// Detection labels that should not be redacted. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub excluded_labels: Vec, /// Replacement applied to every accepted detection. #[serde(default, skip_serializing_if = "Option::is_none")] pub replacement: Option, - /// Whether the provider may use network calls. + /// Whether the worker may use network calls. #[serde(default, skip_serializing_if = "Option::is_none")] pub allow_network: Option, - /// Total provider deadline for one selected payload in milliseconds. + /// Total worker inference deadline for one selected payload in milliseconds. #[serde(default, skip_serializing_if = "Option::is_none")] pub max_latency_ms: Option, } diff --git a/crates/pii-redaction/src/local.rs b/crates/pii-redaction/src/local.rs index d60c56972..0ff9732a9 100644 --- a/crates/pii-redaction/src/local.rs +++ b/crates/pii-redaction/src/local.rs @@ -17,7 +17,7 @@ use nemo_relay::codec::resolve::{ }; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_relay::plugin::{ - InferenceProvider, PluginError, PluginRegistrationContext, Result as PluginResult, + PluginError, PluginRegistrationContext, Result as PluginResult, WorkerInference, }; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -27,9 +27,9 @@ use super::component::{ DEFAULT_LOCAL_MODEL_LATENCY_MS, DEFAULT_LOCAL_MODEL_MIN_SCORE, LocalBackendConfig, MAX_LOCAL_MODEL_EXCLUDED_LABELS, MAX_LOCAL_MODEL_LABEL_BYTES, MAX_LOCAL_MODEL_LATENCY_MS, MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES, MAX_LOCAL_MODEL_REPLACEMENT_BYTES, - MAX_LOCAL_MODEL_TARGET_PATH_BYTES, MAX_LOCAL_MODEL_TARGET_PATHS, - PII_DETECTION_PROVIDER_CONTRACT, PiiRedactionConfig, is_valid_json_pointer, - is_valid_json_pointer_pattern, profile_registration_prefix, + MAX_LOCAL_MODEL_TARGET_PATH_BYTES, MAX_LOCAL_MODEL_TARGET_PATHS, PII_DETECTION_CONTRACT, + PiiRedactionConfig, is_valid_json_pointer, is_valid_json_pointer_pattern, + profile_registration_prefix, }; use super::overlay::BuiltinCodecName; @@ -43,8 +43,8 @@ const MAX_DETECTIONS_PER_TEXT: usize = 128; #[derive(Clone)] struct CompiledLocalBackend { - provider_name: Arc, - provider: InferenceProvider, + inference_name: Arc, + inference: WorkerInference, model_id: Option, detector_profile: Option, target_paths: Arc>>, @@ -131,11 +131,11 @@ impl CompiledLocalBackend { if let Some(violation) = validate_local_backend_config(&config).into_iter().next() { return Err(PluginError::InvalidConfig(violation.message)); } - let provider_name = config + let inference_name = config .backend .as_deref() .map(str::trim) - .expect("validated local backend has a provider name") + .expect("validated local backend has a worker inference name") .to_string(); let min_score = config.min_score.unwrap_or(DEFAULT_LOCAL_MODEL_MIN_SCORE); let replacement = config @@ -150,16 +150,16 @@ impl CompiledLocalBackend { })?), None => None, }; - let provider = ctx - .inference_provider(&provider_name, PII_DETECTION_PROVIDER_CONTRACT) + let inference = ctx + .worker_inference(&inference_name, PII_DETECTION_CONTRACT) .map_err(|error| { PluginError::RegistrationFailed(format!( - "PII redaction inference provider '{provider_name}' is unavailable: {error}" + "PII detection worker '{inference_name}' is unavailable: {error}" )) })?; Ok(Self { - provider_name: Arc::new(provider_name), - provider, + inference_name: Arc::new(inference_name), + inference, model_id: config.model_id.map(|value| value.trim().to_string()), detector_profile: config .detector_profile @@ -361,12 +361,12 @@ impl CompiledLocalBackend { Err(_) => { log::warn!( target: "nemo_relay.plugin", - event = "local_model_provider_failed", + event = "local_model_inference_failed", plugin_kind = "pii_redaction", - provider = self.provider_name.as_str(), + worker_inference = self.inference_name.as_str(), batch_size = batch.len(), - reason = "provider_or_response"; - "PII local-model provider failed closed" + reason = "inference_or_response"; + "PII local-model inference failed closed" ); for index in batch { texts[*index].text = self.replacement.as_str().to_string(); @@ -396,10 +396,10 @@ impl CompiledLocalBackend { .collect(), }; let request = serde_json::to_value(request)?; - let response = self.provider.invoke(request, timeout)?; + let response = self.inference.invoke(request, timeout)?; let response: LocalModelResponse = serde_json::from_value(response).map_err(|error| { PluginError::RegistrationFailed(format!( - "local-model provider returned an invalid detection response: {error}" + "PII detection worker returned an invalid response: {error}" )) })?; self.apply_response(texts, batch, response) @@ -576,7 +576,7 @@ impl CompiledLocalBackend { target: "nemo_relay.plugin", event = "local_model_codec_failed", plugin_kind = "pii_redaction", - provider = self.provider_name.as_str(), + worker_inference = self.inference_name.as_str(), direction, codec_kind, reason; @@ -845,7 +845,7 @@ pub(super) fn validate_local_backend_config( if config.allow_network == Some(true) { push( "local.allow_network", - "worker-backed local-model providers must not use network inference".into(), + "worker-backed local models must not use network inference".into(), ); } match config.max_latency_ms { diff --git a/crates/pii-redaction/tests/unit/component_tests.rs b/crates/pii-redaction/tests/unit/component_tests.rs index 51d86a0d6..61fb59c5e 100644 --- a/crates/pii-redaction/tests/unit/component_tests.rs +++ b/crates/pii-redaction/tests/unit/component_tests.rs @@ -28,11 +28,11 @@ use crate::codec::openai_responses::OpenAIResponsesCodec; use crate::codec::request::AnnotatedLlmRequest; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::plugin::{ - ConfigPolicy, DiagnosticLevel, InferenceProviderDescriptor, InferenceProviderRegistration, - InferenceProviderRegistry, PluginComponentSpec, PluginConfig, PluginError, - PluginRegistrationContext, UnsupportedBehavior, clear_plugin_configuration, + ConfigPolicy, DiagnosticLevel, PluginComponentSpec, PluginConfig, PluginError, + PluginRegistrationContext, UnsupportedBehavior, WorkerInferenceDescriptor, + WorkerInferenceRegistration, WorkerInferenceRegistry, clear_plugin_configuration, ensure_builtin_plugins_registered, initialize_plugins_exact as initialize_plugins, - initialize_plugins_with_inference_providers, list_plugin_kinds, rollback_registrations, + initialize_plugins_with_worker_inference, list_plugin_kinds, rollback_registrations, validate_plugin_config, }; use futures::StreamExt; @@ -148,22 +148,22 @@ fn reset_runtime() { register_pii_redaction_component().unwrap(); } -struct InferenceProviderGuard { - _registration: InferenceProviderRegistration, +struct WorkerInferenceGuard { + _registration: WorkerInferenceRegistration, } -fn register_test_inference_provider( - registry: &InferenceProviderRegistry, +fn register_test_worker_inference( + registry: &WorkerInferenceRegistry, name: &str, callback: impl Fn(Json, std::time::Duration) -> Result + Send + Sync + 'static, -) -> InferenceProviderGuard { +) -> WorkerInferenceGuard { let registration = registry .register( - InferenceProviderDescriptor::new(name, PII_DETECTION_PROVIDER_CONTRACT).unwrap(), + WorkerInferenceDescriptor::new(name, PII_DETECTION_CONTRACT).unwrap(), Arc::new(callback), ) .unwrap(); - InferenceProviderGuard { + WorkerInferenceGuard { _registration: registration, } } @@ -1326,12 +1326,12 @@ fn deterministic_and_local_model_profiles_compose_in_priority_order() { reset_runtime(); setup_isolated_thread(); - let inference_providers = InferenceProviderRegistry::default(); - let _provider = - register_test_inference_provider(&inference_providers, "contextual", |request, _| { + let worker_inference = WorkerInferenceRegistry::default(); + let _registration = + register_test_worker_inference(&worker_inference, "contextual", |request, _| { assert_eq!( request["texts"][0]["text"], "Alice emailed [REDACTED]", - "the local provider must receive the deterministic profile's output" + "the local worker must receive the deterministic profile's output" ); Ok(json!({ "version": 1, @@ -1345,7 +1345,7 @@ fn deterministic_and_local_model_profiles_compose_in_priority_order() { })) }); - futures::executor::block_on(initialize_plugins_with_inference_providers( + futures::executor::block_on(initialize_plugins_with_worker_inference( plugin_config(json!({ "codec": "openai_chat", "profiles": [ @@ -1367,7 +1367,7 @@ fn deterministic_and_local_model_profiles_compose_in_priority_order() { } ] })), - inference_providers, + worker_inference, )) .unwrap(); @@ -1502,11 +1502,11 @@ fn local_profile_registrations_receive_generated_namespaces() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); - let inference_providers = InferenceProviderRegistry::default(); - let _one = register_test_inference_provider(&inference_providers, "one", |_, _| { + let worker_inference = WorkerInferenceRegistry::default(); + let _one = register_test_worker_inference(&worker_inference, "one", |_, _| { Ok(json!({"version": 1, "detections": []})) }); - let _two = register_test_inference_provider(&inference_providers, "two", |_, _| { + let _two = register_test_worker_inference(&worker_inference, "two", |_, _| { Ok(json!({"version": 1, "detections": []})) }); @@ -1521,9 +1521,9 @@ fn local_profile_registrations_receive_generated_namespaces() { let Json::Object(config) = config else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_inference_providers( + let mut ctx = PluginRegistrationContext::with_worker_inference( Some("profiles::".into()), - inference_providers, + worker_inference, ); futures::executor::block_on(plugin.register(&config, &mut ctx)).unwrap(); let mut registrations = ctx.into_registrations(); @@ -1835,7 +1835,7 @@ fn validate_rejects_local_section_outside_local_mode() { } #[test] -fn validate_rejects_invalid_local_model_provider_settings() { +fn validate_rejects_invalid_local_model_worker_settings() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); @@ -2070,24 +2070,21 @@ fn validate_rejects_unknown_builtin_detector() { } #[test] -fn local_backend_provider_is_invoked_for_local_model_mode() { +fn local_backend_worker_inference_is_invoked_for_local_model_mode() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); let called = Arc::new(AtomicBool::new(false)); let called_inner = Arc::clone(&called); - let inference_providers = InferenceProviderRegistry::default(); - let _provider = register_test_inference_provider( - &inference_providers, - "test-provider", - move |request, _| { + let worker_inference = WorkerInferenceRegistry::default(); + let _registration = + register_test_worker_inference(&worker_inference, "test-inference", move |request, _| { called_inner.store(true, Ordering::SeqCst); assert_eq!(request["version"], 1); Ok(json!({"version": 1, "detections": []})) - }, - ); + }); setup_isolated_thread(); - futures::executor::block_on(initialize_plugins_with_inference_providers( + futures::executor::block_on(initialize_plugins_with_worker_inference( plugin_config(json!({ "mode": "local_model", "input": false, @@ -2095,9 +2092,9 @@ fn local_backend_provider_is_invoked_for_local_model_mode() { "mark": false, "tool_input": true, "tool_output": false, - "local": {"backend": "test-provider"} + "local": {"backend": "test-inference"} })), - inference_providers, + worker_inference, )) .unwrap(); tool_call( @@ -2112,7 +2109,7 @@ fn local_backend_provider_is_invoked_for_local_model_mode() { } #[test] -fn local_backend_reports_missing_and_failed_provider_initialization() { +fn local_backend_reports_missing_and_failed_worker_inference_initialization() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); @@ -2131,12 +2128,14 @@ fn local_backend_reports_missing_and_failed_provider_initialization() { }; let mut ctx = PluginRegistrationContext::with_namespace("missing::"); let missing = futures::executor::block_on(plugin.register(&config, &mut ctx)) - .expect_err("missing local provider should fail registration"); + .expect_err("missing worker inference should fail registration"); assert!(missing.to_string().contains("unavailable")); - let inference_providers = InferenceProviderRegistry::default(); - let _failed = register_test_inference_provider(&inference_providers, "failed", |_, _| { - Err(PluginError::RegistrationFailed("provider failed".into())) + let worker_inference = WorkerInferenceRegistry::default(); + let _failed = register_test_worker_inference(&worker_inference, "failed", |_, _| { + Err(PluginError::RegistrationFailed( + "worker inference failed".into(), + )) }); let config = json!({ "mode": "local_model", @@ -2150,25 +2149,23 @@ fn local_backend_reports_missing_and_failed_provider_initialization() { let Json::Object(config) = config else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_inference_providers( - Some("failed::".into()), - inference_providers, - ); + let mut ctx = + PluginRegistrationContext::with_worker_inference(Some("failed::".into()), worker_inference); futures::executor::block_on(plugin.register(&config, &mut ctx)) - .expect("provider availability should be checked at registration"); + .expect("worker inference availability should be checked at registration"); let mut registrations = ctx.into_registrations(); rollback_registrations(&mut registrations); } #[test] -fn local_backend_rejects_provider_with_incompatible_contract() { +fn local_backend_rejects_worker_inference_with_incompatible_contract() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); - let inference_providers = InferenceProviderRegistry::default(); - let _registration = inference_providers + let worker_inference = WorkerInferenceRegistry::default(); + let _registration = worker_inference .register( - InferenceProviderDescriptor::new("embedding", "acme.embedding.v1").unwrap(), + WorkerInferenceDescriptor::new("embedding", "acme.embedding.v1").unwrap(), Arc::new(|request, _| Ok(request)), ) .unwrap(); @@ -2184,16 +2181,16 @@ fn local_backend_rejects_provider_with_incompatible_contract() { }) else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_inference_providers( + let mut ctx = PluginRegistrationContext::with_worker_inference( Some("mismatch::".into()), - inference_providers, + worker_inference, ); let error = futures::executor::block_on(plugin.register(&config, &mut ctx)) - .expect_err("PII must reject providers implementing another contract"); + .expect_err("PII must reject worker inference implementing another contract"); assert!(error.to_string().contains("acme.embedding.v1")); - assert!(error.to_string().contains(PII_DETECTION_PROVIDER_CONTRACT)); + assert!(error.to_string().contains(PII_DETECTION_CONTRACT)); } #[test] diff --git a/crates/pii-redaction/tests/unit/local_tests.rs b/crates/pii-redaction/tests/unit/local_tests.rs index 4669b8422..11336fdba 100644 --- a/crates/pii-redaction/tests/unit/local_tests.rs +++ b/crates/pii-redaction/tests/unit/local_tests.rs @@ -12,15 +12,15 @@ use nemo_relay::codec::resolve::{ }; use nemo_relay::codec::traits::LlmCodec; use nemo_relay::plugin::{ - InferenceProviderDescriptor, InferenceProviderRegistration, InferenceProviderRegistry, - PluginRegistrationContext, + PluginRegistrationContext, WorkerInferenceDescriptor, WorkerInferenceRegistration, + WorkerInferenceRegistry, }; use serde_json::json; use super::*; -struct ProviderGuard { - _registration: InferenceProviderRegistration, +struct WorkerInferenceGuard { + _registration: WorkerInferenceRegistration, } struct IdentifiedRequestCodec { @@ -45,30 +45,30 @@ impl LlmCodec for IdentifiedRequestCodec { } } -fn provider_context( +fn worker_inference_context( name: &'static str, callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, -) -> (ProviderGuard, PluginRegistrationContext) { - let registry = InferenceProviderRegistry::default(); +) -> (WorkerInferenceGuard, PluginRegistrationContext) { + let registry = WorkerInferenceRegistry::default(); let registration = registry .register( - InferenceProviderDescriptor::new(name, PII_DETECTION_PROVIDER_CONTRACT).unwrap(), + WorkerInferenceDescriptor::new(name, PII_DETECTION_CONTRACT).unwrap(), Arc::new(callback), ) .unwrap(); ( - ProviderGuard { + WorkerInferenceGuard { _registration: registration, }, - PluginRegistrationContext::with_inference_providers(None, registry), + PluginRegistrationContext::with_worker_inference(None, registry), ) } fn backend( name: &'static str, callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, -) -> (ProviderGuard, CompiledLocalBackend) { - let (provider, ctx) = provider_context(name, callback); +) -> (WorkerInferenceGuard, CompiledLocalBackend) { + let (registration, ctx) = worker_inference_context(name, callback); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some(name.into()), @@ -78,14 +78,14 @@ fn backend( &ctx, ) .unwrap(); - (provider, backend) + (registration, backend) } fn alice_detector(request: Json, _timeout: Duration) -> PluginResult { let mut detections = Vec::new(); for item in request["texts"] .as_array() - .expect("provider request should contain texts") + .expect("detection request should contain texts") { let text_id = item["id"].as_u64().expect("text id should be an integer"); let text = item["text"].as_str().expect("text should be a string"); @@ -104,7 +104,7 @@ fn alice_detector(request: Json, _timeout: Duration) -> PluginResult { #[test] fn applies_non_overlapping_utf8_byte_spans() { - let (_provider, backend) = backend("local-test-utf8", |_, _| { + let (_registration, backend) = backend("local-test-utf8", |_, _| { Ok(json!({ "version": 1, "detections": [ @@ -134,7 +134,7 @@ fn applies_non_overlapping_utf8_byte_spans() { #[test] fn malformed_or_overlapping_spans_fail_closed_for_the_batch() { - let (_provider, backend) = backend("local-test-overlap", |_, _| { + let (_registration, backend) = backend("local-test-overlap", |_, _| { Ok(json!({ "version": 1, "detections": [ @@ -163,8 +163,8 @@ fn malformed_or_overlapping_spans_fail_closed_for_the_batch() { } #[test] -fn provider_errors_fail_closed_without_changing_unselected_paths() { - let (_provider, ctx) = provider_context("local-test-failure", |_, _| { +fn worker_inference_errors_fail_closed_without_changing_unselected_paths() { + let (_registration, ctx) = worker_inference_context("local-test-failure", |_, _| { Err(PluginError::RegistrationFailed("boom".into())) }); let backend = CompiledLocalBackend::new( @@ -192,10 +192,10 @@ fn provider_errors_fail_closed_without_changing_unselected_paths() { } #[test] -fn batches_provider_requests_and_preserves_no_detection_values() { +fn batches_detection_requests_and_preserves_no_detection_values() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, backend) = backend("local-test-batching", move |request, _| { + let (_registration, backend) = backend("local-test-batching", move |request, _| { observed.fetch_add(1, Ordering::SeqCst); assert!(request["texts"].as_array().unwrap().len() <= MAX_BATCH_ITEMS); Ok(json!({"version": 1, "detections": []})) @@ -215,10 +215,10 @@ fn batches_provider_requests_and_preserves_no_detection_values() { } #[test] -fn batches_multiple_event_roots_into_one_provider_request() { +fn batches_multiple_event_roots_into_one_detection_request() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, backend) = backend("local-test-multi-root", move |request, _| { + let (_registration, backend) = backend("local-test-multi-root", move |request, _| { observed.fetch_add(1, Ordering::SeqCst); assert_eq!(request["texts"].as_array().unwrap().len(), 3); Ok(json!({"version": 1, "detections": []})) @@ -242,10 +242,10 @@ fn batches_multiple_event_roots_into_one_provider_request() { } #[test] -fn event_callback_batches_all_selected_fields_into_one_provider_request() { +fn event_callback_batches_all_selected_fields_into_one_detection_request() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, backend) = backend("local-test-event-batching", move |request, _| { + let (_registration, backend) = backend("local-test-event-batching", move |request, _| { observed.fetch_add(1, Ordering::SeqCst); assert_eq!(request["texts"].as_array().unwrap().len(), 3); Ok(json!({"version": 1, "detections": []})) @@ -279,11 +279,12 @@ fn event_callback_batches_all_selected_fields_into_one_provider_request() { fn latency_budget_applies_to_the_entire_payload() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, ctx) = provider_context("local-test-total-deadline", move |_, timeout| { - observed.fetch_add(1, Ordering::SeqCst); - std::thread::sleep(timeout + Duration::from_millis(5)); - Err(PluginError::RegistrationFailed("timed out".into())) - }); + let (_registration, ctx) = + worker_inference_context("local-test-total-deadline", move |_, timeout| { + observed.fetch_add(1, Ordering::SeqCst); + std::thread::sleep(timeout + Duration::from_millis(5)); + Err(PluginError::RegistrationFailed("timed out".into())) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-total-deadline".into()), @@ -311,10 +312,10 @@ fn latency_budget_applies_to_the_entire_payload() { } #[test] -fn oversized_text_is_redacted_without_calling_the_provider() { +fn oversized_text_is_redacted_without_calling_the_registration() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, backend) = backend("local-test-oversized", move |_, _| { + let (_registration, backend) = backend("local-test-oversized", move |_, _| { observed.fetch_add(1, Ordering::SeqCst); Ok(json!({"version": 1, "detections": []})) }); @@ -327,8 +328,8 @@ fn oversized_text_is_redacted_without_calling_the_provider() { } #[test] -fn oversized_text_does_not_shift_later_provider_results() { - let (_provider, backend) = backend("local-test-oversized-middle", |_, _| { +fn oversized_text_does_not_shift_later_inference_results() { + let (_registration, backend) = backend("local-test-oversized-middle", |_, _| { Ok(json!({"version": 1, "detections": []})) }); @@ -339,10 +340,10 @@ fn oversized_text_does_not_shift_later_provider_results() { } #[test] -fn payload_count_limit_fails_closed_without_unbounded_provider_calls() { +fn payload_count_limit_fails_closed_without_unbounded_inference_calls() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, backend) = backend("local-test-count-limit", move |_, _| { + let (_registration, backend) = backend("local-test-count-limit", move |_, _| { observed.fetch_add(1, Ordering::SeqCst); Ok(json!({"version": 1, "detections": []})) }); @@ -364,7 +365,7 @@ fn payload_count_limit_fails_closed_without_unbounded_provider_calls() { fn payload_byte_limit_fails_closed_after_the_bounded_prefix() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, backend) = backend("local-test-byte-limit", move |_, _| { + let (_registration, backend) = backend("local-test-byte-limit", move |_, _| { observed.fetch_add(1, Ordering::SeqCst); Ok(json!({"version": 1, "detections": []})) }); @@ -385,7 +386,7 @@ fn payload_byte_limit_fails_closed_after_the_bounded_prefix() { #[test] fn non_utf8_boundary_detection_fails_closed() { - let (_provider, backend) = backend("local-test-utf8-boundary", |_, _| { + let (_registration, backend) = backend("local-test-utf8-boundary", |_, _| { Ok(json!({ "version": 1, "detections": [{ @@ -406,7 +407,8 @@ fn non_utf8_boundary_detection_fails_closed() { #[test] fn local_policy_rejects_malformed_or_unbounded_values() { - let (_provider, ctx) = provider_context("local-test-policy-bounds", |request, _| Ok(request)); + let (_registration, ctx) = + worker_inference_context("local-test-policy-bounds", |request, _| Ok(request)); for (config, expected) in [ ( @@ -488,7 +490,7 @@ fn local_policy_accepts_root_and_escaped_json_pointers() { #[test] fn target_path_patterns_match_one_segment_without_widening_exact_paths() { - let (_provider, ctx) = provider_context("local-test-path-patterns", alice_detector); + let (_registration, ctx) = worker_inference_context("local-test-path-patterns", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-path-patterns".into()), @@ -523,7 +525,7 @@ fn target_path_patterns_match_one_segment_without_widening_exact_paths() { #[test] fn exact_paths_match_escaped_object_keys() { - let (_provider, ctx) = provider_context("local-test-escaped-path", alice_detector); + let (_registration, ctx) = worker_inference_context("local-test-escaped-path", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-escaped-path".into()), @@ -551,10 +553,11 @@ fn exact_paths_match_escaped_object_keys() { fn raw_request_paths_batch_headers_and_content_without_a_codec() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); - let (_provider, ctx) = provider_context("local-test-raw-request", move |request, timeout| { - observed.fetch_add(1, Ordering::SeqCst); - alice_detector(request, timeout) - }); + let (_registration, ctx) = + worker_inference_context("local-test-raw-request", move |request, timeout| { + observed.fetch_add(1, Ordering::SeqCst); + alice_detector(request, timeout) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-raw-request".into()), @@ -583,7 +586,7 @@ fn raw_request_paths_batch_headers_and_content_without_a_codec() { #[test] fn raw_response_paths_work_without_a_codec() { - let (_provider, ctx) = provider_context("local-test-raw-response", alice_detector); + let (_registration, ctx) = worker_inference_context("local-test-raw-response", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-raw-response".into()), @@ -607,7 +610,8 @@ fn raw_response_paths_work_without_a_codec() { #[test] fn request_codec_classifies_only_normalized_content_patterns() { - let (_provider, ctx) = provider_context("local-test-openai-request", alice_detector); + let (_registration, ctx) = + worker_inference_context("local-test-openai-request", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-openai-request".into()), @@ -672,7 +676,8 @@ fn request_codec_classifies_only_normalized_content_patterns() { #[test] fn request_uses_the_active_codec_instead_of_the_legacy_fallback() { - let (_provider, ctx) = provider_context("local-test-active-request-codec", alice_detector); + let (_registration, ctx) = + worker_inference_context("local-test-active-request-codec", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-active-request-codec".into()), @@ -711,7 +716,8 @@ fn request_uses_the_active_codec_instead_of_the_legacy_fallback() { #[test] fn response_codec_classifies_message_content_without_touching_identity_fields() { - let (_provider, ctx) = provider_context("local-test-openai-response", alice_detector); + let (_registration, ctx) = + worker_inference_context("local-test-openai-response", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-openai-response".into()), @@ -752,7 +758,8 @@ fn response_codec_classifies_message_content_without_touching_identity_fields() #[test] fn request_codec_failure_omits_the_observable_body() { - let (_provider, ctx) = provider_context("local-test-invalid-openai-request", alice_detector); + let (_registration, ctx) = + worker_inference_context("local-test-invalid-openai-request", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-invalid-openai-request".into()), @@ -787,7 +794,8 @@ fn request_codec_failure_omits_the_observable_body() { #[test] fn request_codec_ambiguous_multi_message_edit_fails_closed() { - let (_provider, ctx) = provider_context("local-test-ambiguous-openai-request", alice_detector); + let (_registration, ctx) = + worker_inference_context("local-test-ambiguous-openai-request", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-ambiguous-openai-request".into()), @@ -821,7 +829,8 @@ fn request_codec_ambiguous_multi_message_edit_fails_closed() { #[test] fn response_codec_failure_omits_the_observable_payload() { - let (_provider, ctx) = provider_context("local-test-invalid-openai-response", alice_detector); + let (_registration, ctx) = + worker_inference_context("local-test-invalid-openai-response", alice_detector); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-invalid-openai-response".into()), @@ -850,7 +859,7 @@ fn response_codec_failure_omits_the_observable_payload() { #[test] fn host_policy_applies_score_threshold_and_label_exclusions() { - let (_provider, ctx) = provider_context("local-test-detection-policy", |_, _| { + let (_registration, ctx) = worker_inference_context("local-test-detection-policy", |_, _| { Ok(json!({ "version": 1, "detections": [ @@ -898,18 +907,19 @@ fn host_policy_applies_score_threshold_and_label_exclusions() { #[test] fn validates_filtered_detections_before_applying_host_policy() { - let (_provider, ctx) = provider_context("local-test-filtered-invalid-span", |_, _| { - Ok(json!({ - "version": 1, - "detections": [{ - "text_id": 0, - "start_utf8": 0, - "end_utf8": 999, - "label": "LOW_SCORE", - "score": 0.1 - }] - })) - }); + let (_registration, ctx) = + worker_inference_context("local-test-filtered-invalid-span", |_, _| { + Ok(json!({ + "version": 1, + "detections": [{ + "text_id": 0, + "start_utf8": 0, + "end_utf8": 999, + "label": "LOW_SCORE", + "score": 0.1 + }] + })) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-filtered-invalid-span".into()), @@ -929,20 +939,21 @@ fn validates_filtered_detections_before_applying_host_policy() { #[test] fn enforces_detection_limit_for_each_text() { - let (_provider, ctx) = provider_context("local-test-per-text-detection-limit", |_, _| { - let detections = (0..=MAX_DETECTIONS_PER_TEXT) - .map(|index| { - json!({ - "text_id": 0, - "start_utf8": index, - "end_utf8": index + 1, - "label": "NAME", - "score": 0.9 + let (_registration, ctx) = + worker_inference_context("local-test-per-text-detection-limit", |_, _| { + let detections = (0..=MAX_DETECTIONS_PER_TEXT) + .map(|index| { + json!({ + "text_id": 0, + "start_utf8": index, + "end_utf8": index + 1, + "label": "NAME", + "score": 0.9 + }) }) - }) - .collect::>(); - Ok(json!({"version": 1, "detections": detections})) - }); + .collect::>(); + Ok(json!({"version": 1, "detections": detections})) + }); let backend = CompiledLocalBackend::new( LocalBackendConfig { backend: Some("local-test-per-text-detection-limit".into()), diff --git a/crates/pii-redaction/tests/worker_provider_tests.rs b/crates/pii-redaction/tests/worker_detection_tests.rs similarity index 91% rename from crates/pii-redaction/tests/worker_provider_tests.rs rename to crates/pii-redaction/tests/worker_detection_tests.rs index 3d6df4ee4..d402f5f12 100644 --- a/crates/pii-redaction/tests/worker_provider_tests.rs +++ b/crates/pii-redaction/tests/worker_detection_tests.rs @@ -19,7 +19,7 @@ use nemo_relay::plugin::dynamic::{ }; use nemo_relay::plugin::{PluginComponentSpec, PluginConfig, clear_plugin_configuration}; use nemo_relay_pii_redaction::component::{ - PII_DETECTION_PROVIDER_CONTRACT, PII_REDACTION_PLUGIN_KIND, register_pii_redaction_component, + PII_DETECTION_CONTRACT, PII_REDACTION_PLUGIN_KIND, register_pii_redaction_component, }; use serde_json::{Map, json}; use tempfile::TempDir; @@ -27,7 +27,7 @@ use tempfile::TempDir; static WORKER_PII_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); #[tokio::test(flavor = "multi_thread")] -async fn worker_provider_sanitizes_events_and_is_removed_after_host_clear() { +async fn worker_detection_sanitizes_events_and_is_removed_after_host_clear() { let _guard = WORKER_PII_TEST_LOCK.lock().await; let _ = clear_plugin_configuration(); register_pii_redaction_component().expect("PII component should register"); @@ -69,19 +69,16 @@ async fn worker_provider_sanitizes_events_and_is_removed_after_host_clear() { kind: DynamicPluginKind::Worker, manifest_ref: manifest_ref.to_string_lossy().into_owned(), environment_ref: None, - config: Map::from_iter([("provider_only".into(), json!(true))]), + config: Map::from_iter([("worker_inference_only".into(), json!(true))]), }], ) .await .expect("worker and PII component should activate together"); assert!(!report.has_errors()); - let inference_providers = activation.inference_providers(); + let worker_inference = activation.worker_inference_registry(); assert!( - inference_providers - .resolve( - "fixture_worker/fixture_local_model", - PII_DETECTION_PROVIDER_CONTRACT, - ) + worker_inference + .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT,) .is_ok() ); @@ -204,18 +201,15 @@ async fn worker_provider_sanitizes_events_and_is_removed_after_host_clear() { deregister_subscriber(subscriber_name).expect("test subscriber should deregister"); activation.clear().expect("plugin host should clear"); assert!( - inference_providers - .resolve( - "fixture_worker/fixture_local_model", - PII_DETECTION_PROVIDER_CONTRACT, - ) + worker_inference + .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT,) .is_err(), - "worker provider should not outlive its host activation" + "worker inference should not outlive its host activation" ); } #[tokio::test(flavor = "multi_thread")] -async fn worker_exit_during_sanitization_fails_closed_and_removes_provider() { +async fn worker_exit_during_sanitization_fails_closed_and_removes_inference() { let _guard = WORKER_PII_TEST_LOCK.lock().await; let _ = clear_plugin_configuration(); register_pii_redaction_component().expect("PII component should register"); @@ -252,7 +246,7 @@ async fn worker_exit_during_sanitization_fails_closed_and_removes_provider() { manifest_ref: manifest_ref.to_string_lossy().into_owned(), environment_ref: None, config: Map::from_iter([ - ("provider_only".into(), json!(true)), + ("worker_inference_only".into(), json!(true)), ("exit_in_local_model".into(), json!(true)), ]), }], @@ -260,7 +254,7 @@ async fn worker_exit_during_sanitization_fails_closed_and_removes_provider() { .await .expect("worker and PII component should activate together"); assert!(!report.has_errors()); - let inference_providers = activation.inference_providers(); + let worker_inference = activation.worker_inference_registry(); let events = Arc::new(Mutex::new(Vec::::new())); let captured = Arc::clone(&events); @@ -297,13 +291,10 @@ async fn worker_exit_during_sanitization_fails_closed_and_removes_provider() { .to_string(); assert!(error.contains("shutdown"), "{error}"); assert!( - inference_providers - .resolve( - "fixture_worker/fixture_local_model", - PII_DETECTION_PROVIDER_CONTRACT, - ) + worker_inference + .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT,) .is_err(), - "failed worker provider should not survive host teardown" + "failed worker inference should not survive host teardown" ); } diff --git a/crates/pii-redaction/providers/rampart/MANIFEST.in b/crates/pii-redaction/workers/rampart/MANIFEST.in similarity index 100% rename from crates/pii-redaction/providers/rampart/MANIFEST.in rename to crates/pii-redaction/workers/rampart/MANIFEST.in diff --git a/crates/pii-redaction/providers/rampart/README.md b/crates/pii-redaction/workers/rampart/README.md similarity index 90% rename from crates/pii-redaction/providers/rampart/README.md rename to crates/pii-redaction/workers/rampart/README.md index 91d49d4b6..9a25154b5 100644 --- a/crates/pii-redaction/providers/rampart/README.md +++ b/crates/pii-redaction/workers/rampart/README.md @@ -3,7 +3,7 @@ SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All SPDX-License-Identifier: Apache-2.0 --> -# Rampart PII Provider +# Rampart PII Worker This optional manifest-backed Python worker runs the [`nationaldesignstudio/rampart`](https://huggingface.co/nationaldesignstudio/rampart) @@ -26,7 +26,7 @@ nemo-relay plugins add ./relay-plugin.toml nemo-relay plugins enable nemo_relay.pii_rampart ``` -If the provider package is already installed, run +If the worker package is already installed, run `nemo-relay-pii-rampart-prefetch` directly. `plugins add` creates a separate Relay-managed Python environment from the same source directory. @@ -119,7 +119,7 @@ target_path_patterns = [ ] ``` -`allow_network = false` means provider inference is local. It does not sandbox +`allow_network = false` means worker inference is local. It does not sandbox the worker. `local_files_only` must remain `true`; activation-time model acquisition is not supported. @@ -129,24 +129,24 @@ local-model profile for names and contextual identifiers. Keep the local-model profile limited to normalized content paths. Classifying every string leaf can produce false positives on model names, region names, UUIDs, trace IDs, and other machine identifiers. Relay, not the worker, applies `min_score` and -optional `excluded_labels` policy after validating the provider response. +optional `excluded_labels` policy after validating the worker response. ## Runtime Bounds -- At most 64 texts and 64 KiB of UTF-8 text are accepted per provider request. +- At most 64 texts and 64 KiB of UTF-8 text are accepted per detection request. - Each text is limited to 16 KiB. - Long inputs use overlapping 510-token windows, with 64 content tokens of overlap. - ONNX inference batches and total windows are bounded by worker configuration. -- Requests above the provider bounds return an error; the PII component then +- Requests above the worker bounds return an error; the PII component then fails closed for the affected batch. -- `max_latency_ms` is one total budget for all provider batches selected from +- `max_latency_ms` is one total budget for all inference batches selected from one payload. - CPU inference is serialized per worker process. Host deadlines cancel the RPC, while already-running native inference is allowed to finish before its admission slot is released. - Use a `max_latency_ms` of at least 5000 when the selected payload can approach - the 64 KiB provider-request limit. Smaller content-only payloads normally + the 64 KiB detection-request limit. Smaller content-only payloads normally complete much faster. Benchmark representative inputs on deployment hardware before lowering the deadline. diff --git a/crates/pii-redaction/providers/rampart/THIRD_PARTY_NOTICES.md b/crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md similarity index 82% rename from crates/pii-redaction/providers/rampart/THIRD_PARTY_NOTICES.md rename to crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md index 248eef265..40e22ec71 100644 --- a/crates/pii-redaction/providers/rampart/THIRD_PARTY_NOTICES.md +++ b/crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md @@ -5,14 +5,14 @@ SPDX-License-Identifier: Apache-2.0 # Third-Party Notices -This optional provider downloads and executes **Rampart**, published by +This optional worker downloads and executes **Rampart**, published by National Design Studio at [`nationaldesignstudio/rampart`](https://huggingface.co/nationaldesignstudio/rampart). The model and its training-data attribution are published under the [Creative Commons Attribution 4.0 International license](https://creativecommons.org/licenses/by/4.0/). -The default provider configuration selects model revision +The default worker configuration selects model revision `b1993e4e68b082835b80ffc65acc03325ea2e501`. Model files are downloaded to the operator's Hugging Face cache and are not distributed in the NeMo Relay source or Python package. diff --git a/crates/pii-redaction/providers/rampart/config.schema.json b/crates/pii-redaction/workers/rampart/config.schema.json similarity index 96% rename from crates/pii-redaction/providers/rampart/config.schema.json rename to crates/pii-redaction/workers/rampart/config.schema.json index 43fac3687..11019be45 100644 --- a/crates/pii-redaction/providers/rampart/config.schema.json +++ b/crates/pii-redaction/workers/rampart/config.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "NeMo Relay Rampart PII Provider", + "title": "NeMo Relay Rampart PII Worker", "type": "object", "additionalProperties": false, "properties": { diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py similarity index 84% rename from crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py rename to crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py index 027d78593..78305e3b1 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/__init__.py +++ b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Rampart inference provider for the NeMo Relay PII component.""" +"""Rampart detector worker for the NeMo Relay PII component.""" from .detector import DEFAULT_MODEL_ID, DEFAULT_MODEL_REVISION, RampartDetector, RampartSettings diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py similarity index 99% rename from crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py rename to crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py index 6dd482516..fa5325c19 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/detector.py +++ b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py @@ -210,7 +210,7 @@ def load(cls, settings: RampartSettings) -> RampartDetector: return detector def detect_request(self, request: Any) -> dict[str, Any]: - """Validate one provider request and return versioned UTF-8 spans.""" + """Validate one detection request and return versioned UTF-8 spans.""" texts, requested_model, profile = _parse_request(request) if requested_model is not None and requested_model != DEFAULT_MODEL_ID: raise ValueError(f"request model_id {requested_model!r} does not match loaded model {DEFAULT_MODEL_ID!r}") diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/prefetch.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/prefetch.py similarity index 100% rename from crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/prefetch.py rename to crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/prefetch.py diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/py.typed b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/py.typed similarity index 100% rename from crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/py.typed rename to crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/py.typed diff --git a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py similarity index 90% rename from crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py rename to crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py index d04533e99..5383a7cc9 100644 --- a/crates/pii-redaction/providers/rampart/nemo_relay_pii_rampart/worker.py +++ b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Manifest entrypoint for the Rampart PII inference provider.""" +"""Manifest entrypoint for the Rampart PII worker.""" from __future__ import annotations @@ -12,7 +12,7 @@ from .detector import RampartDetector, RampartSettings, resolve_verified_model_root -PII_DETECTION_PROVIDER_CONTRACT = "nemo.relay.pii_detection.v1" +PII_DETECTION_CONTRACT = "nemo.relay.pii_detection.v1" class _Admission: @@ -22,7 +22,7 @@ def __init__(self, limit: int) -> None: def acquire(self) -> None: if self._active >= self._limit: - raise RuntimeError("Rampart provider is at its pending-request limit") + raise RuntimeError("Rampart worker is at its pending-request limit") self._active += 1 def release(self) -> None: @@ -30,7 +30,7 @@ def release(self) -> None: class RampartWorker(WorkerPlugin): - """Expose Rampart inference through the PII component's provider contract.""" + """Expose Rampart inference through the PII detection contract.""" plugin_id = "nemo_relay.pii_rampart" @@ -100,9 +100,9 @@ def release_after_work(_task: asyncio.Task[Json]) -> None: if not release_on_completion: admission.release() - ctx.register_inference_provider( + ctx.register_worker_inference( "detector", - PII_DETECTION_PROVIDER_CONTRACT, + PII_DETECTION_CONTRACT, detect, ) diff --git a/crates/pii-redaction/providers/rampart/pyproject.toml b/crates/pii-redaction/workers/rampart/pyproject.toml similarity index 94% rename from crates/pii-redaction/providers/rampart/pyproject.toml rename to crates/pii-redaction/workers/rampart/pyproject.toml index c658534cb..f70d467e5 100644 --- a/crates/pii-redaction/providers/rampart/pyproject.toml +++ b/crates/pii-redaction/workers/rampart/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-relay-pii-rampart" version = "0.1.0" -description = "Optional Rampart inference provider for NeMo Relay PII redaction" +description = "Optional Rampart detector worker for NeMo Relay PII redaction" readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0" diff --git a/crates/pii-redaction/providers/rampart/relay-plugin.toml b/crates/pii-redaction/workers/rampart/relay-plugin.toml similarity index 87% rename from crates/pii-redaction/providers/rampart/relay-plugin.toml rename to crates/pii-redaction/workers/rampart/relay-plugin.toml index a83fdaf18..f6e5f1839 100644 --- a/crates/pii-redaction/providers/rampart/relay-plugin.toml +++ b/crates/pii-redaction/workers/rampart/relay-plugin.toml @@ -25,7 +25,7 @@ manifest_root = "." artifact = "nemo_relay_pii_rampart/worker.py" [integrity] -sha256 = "sha256:e0ab2677112b8687d0bee32b257373a4057878f437ec8b66a233cb7b02533338" +sha256 = "sha256:96a3dcf534061db2b5f3146702f433fdb2a10d980230dff13f740cc74897cd2c" [load] runtime = "python" diff --git a/crates/pii-redaction/providers/rampart/tests/test_detector.py b/crates/pii-redaction/workers/rampart/tests/test_detector.py similarity index 100% rename from crates/pii-redaction/providers/rampart/tests/test_detector.py rename to crates/pii-redaction/workers/rampart/tests/test_detector.py diff --git a/crates/pii-redaction/providers/rampart/tests/test_worker.py b/crates/pii-redaction/workers/rampart/tests/test_worker.py similarity index 96% rename from crates/pii-redaction/providers/rampart/tests/test_worker.py rename to crates/pii-redaction/workers/rampart/tests/test_worker.py index e7e7fc0b7..74d5e4433 100644 --- a/crates/pii-redaction/providers/rampart/tests/test_worker.py +++ b/crates/pii-redaction/workers/rampart/tests/test_worker.py @@ -18,7 +18,7 @@ class FakeContext: def __init__(self) -> None: self.callback: Any = None - def register_inference_provider(self, name: str, contract: str, callback: Any) -> None: + def register_worker_inference(self, name: str, contract: str, callback: Any) -> None: assert name == "detector" assert contract == "nemo.relay.pii_detection.v1" self.callback = callback @@ -77,7 +77,7 @@ def fail(_settings: Any) -> None: assert "/sensitive" not in diagnostic.message -def test_worker_registers_async_provider(monkeypatch: pytest.MonkeyPatch) -> None: +def test_worker_registers_async_inference(monkeypatch: pytest.MonkeyPatch) -> None: fake = FakeDetector() monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: fake) context = FakeContext() @@ -117,7 +117,7 @@ async def exercise() -> None: continue assert result["version"] == 1 return - pytest.fail("provider admission was not released after native work completed") + pytest.fail("worker admission was not released after native work completed") asyncio.run(exercise()) diff --git a/crates/worker-proto/README.md b/crates/worker-proto/README.md index 45c2aa3f2..e657eb224 100644 --- a/crates/worker-proto/README.md +++ b/crates/worker-proto/README.md @@ -43,7 +43,7 @@ tooling. clients, servers, services, and messages. - **JSON envelope helpers**: `json_envelope` and `decode_json_envelope` for serializing Relay DTOs into protocol payloads. -- **Language-neutral provider surface**: `INFERENCE_PROVIDER` carries a +- **Language-neutral worker inference**: `WORKER_INFERENCE` carries a versioned contract identifier plus component-owned request and response JSON without coupling the host to the worker implementation language. diff --git a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto index 59d8fb3f7..9f9564e9b 100644 --- a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto +++ b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto @@ -52,7 +52,7 @@ enum RegistrationSurface { MARK_SANITIZE_GUARDRAIL = 30; SCOPE_SANITIZE_START_GUARDRAIL = 31; SCOPE_SANITIZE_END_GUARDRAIL = 32; - INFERENCE_PROVIDER = 40; + WORKER_INFERENCE = 40; } enum LlmCodecKind { @@ -159,7 +159,7 @@ message InvokeRequest { JsonEnvelope event = 10; ToolInvocation tool = 11; LlmInvocation llm = 12; - JsonEnvelope provider = 13; + JsonEnvelope worker_inference = 13; } } diff --git a/crates/worker-proto/tests/proto_tests.rs b/crates/worker-proto/tests/proto_tests.rs index 6722c23ee..ba315566f 100644 --- a/crates/worker-proto/tests/proto_tests.rs +++ b/crates/worker-proto/tests/proto_tests.rs @@ -42,13 +42,29 @@ fn registration_surface_values_are_stable() { assert_eq!(RegistrationSurface::MarkSanitizeGuardrail as i32, 30); assert_eq!(RegistrationSurface::ScopeSanitizeStartGuardrail as i32, 31); assert_eq!(RegistrationSurface::ScopeSanitizeEndGuardrail as i32, 32); - assert_eq!(RegistrationSurface::InferenceProvider as i32, 40); + assert_eq!(RegistrationSurface::WorkerInference as i32, 40); let encoded = Registration { contract: "x".into(), ..Default::default() } .encode_to_vec(); assert_eq!(encoded, vec![42, 1, b'x']); + + let encoded = InvokeRequest { + surface: RegistrationSurface::WorkerInference as i32, + payload: Some( + nemo_relay_worker_proto::v1::invoke_request::Payload::WorkerInference(JsonEnvelope { + schema: "s".into(), + json: b"{}".to_vec(), + }), + ), + ..Default::default() + } + .encode_to_vec(); + assert_eq!( + encoded, + vec![32, 40, 106, 7, 10, 1, b's', 18, 2, b'{', b'}'] + ); } #[test] diff --git a/crates/worker/README.md b/crates/worker/README.md index 16bb6fa9c..439139be9 100644 --- a/crates/worker/README.md +++ b/crates/worker/README.md @@ -25,7 +25,7 @@ communicates with Relay through the versioned `grpc-v1` worker protocol. - **Isolate plugin code**: Run custom runtime behavior outside the Relay host process. - **Use typed registration APIs**: Implement `WorkerPlugin` and register - subscribers, guardrails, intercepts, or inference providers with + subscribers, guardrails, intercepts, or worker inference callbacks with `PluginContext`. - **Call the host runtime**: Emit marks, manage scopes, and invoke middleware continuations through `PluginRuntime`. @@ -86,13 +86,13 @@ Relay supplies the socket, activation ID, and authentication token through the worker environment. Use `serve_plugin` for Relay-spawned workers; explicit server configuration is intended for tests and custom launchers. -## Inference Providers +## Worker Inference -A worker can expose detector or inference functionality to a first-party host -component without owning middleware policy: +A worker can expose contract-scoped inference to a first-party host component +without owning middleware policy: ```rust -ctx.register_inference_provider( +ctx.register_worker_inference( "detector", "acme.pii_detection.v1", |request| async move { @@ -104,7 +104,7 @@ ctx.register_inference_provider( ); ``` -The host publishes the provider as `/detector`. The consuming +The host publishes the callback as `/detector`. The consuming component selects the exact contract and owns the request and response schema, deadline, field selection, validation, and application of the result. The worker callback should perform inference only. diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 900e0b5e9..75f0cff38 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -270,7 +270,7 @@ type LlmRequestFn = Arc< type LlmExecutionFn = Arc BoxFutureResult + Send + Sync>; type LlmStreamExecutionFn = Arc BoxFutureResult + Send + Sync>; -type InferenceProviderFn = Arc BoxFutureResult + Send + Sync>; +type WorkerInferenceFn = Arc BoxFutureResult + Send + Sync>; #[derive(Default)] struct WorkerHandlers { @@ -290,7 +290,7 @@ struct WorkerHandlers { llm_requests: HashMap, llm_executions: HashMap, llm_stream_executions: HashMap, - inference_providers: HashMap, + worker_inference: HashMap, } /// Registration context passed to [`WorkerPlugin::register`]. @@ -332,18 +332,18 @@ impl PluginContext { .insert(name.into(), Arc::new(callback)); } - /// Registers a named inference provider for a versioned host contract. + /// Registers named worker inference for a versioned host contract. /// - /// The provider receives and returns versioned JSON data owned by the + /// The callback receives and returns versioned JSON data owned by the /// consuming host component. It does not register middleware or decide /// which runtime fields are sanitized. - pub fn register_inference_provider(&mut self, name: &str, contract: &str, callback: F) + pub fn register_worker_inference(&mut self, name: &str, contract: &str, callback: F) where F: Fn(Json) -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, { - self.push_contract_registration(name, RegistrationSurface::InferenceProvider, contract); - self.handlers.inference_providers.insert( + self.push_contract_registration(name, RegistrationSurface::WorkerInference, contract); + self.handlers.worker_inference.insert( name.into(), Arc::new(move |request| Box::pin(callback(request))), ); @@ -1696,9 +1696,9 @@ impl WorkerService { | RegistrationSurface::LlmExecutionIntercept => { self.invoke_llm_response(request, &scope, surface).await } - RegistrationSurface::InferenceProvider => { - let payload = provider_payload(request.payload)?; - let handler = self.inference_provider(&request.registration_name)?; + RegistrationSurface::WorkerInference => { + let payload = worker_inference_payload(request.payload)?; + let handler = self.worker_inference(&request.registration_name)?; let future = with_thread_scope(&scope, || handler(payload)); Ok(json_response(future.await?)) } @@ -2098,15 +2098,15 @@ impl WorkerService { }) } - fn inference_provider(&self, name: &str) -> Result { + fn worker_inference(&self, name: &str) -> Result { self.handlers .lock() .map_err(|err| WorkerSdkError::Callback(format!("handler lock poisoned: {err}")))? - .inference_providers + .worker_inference .get(name) .cloned() .ok_or_else(|| { - WorkerSdkError::InvalidInput(format!("inference provider '{name}' not registered")) + WorkerSdkError::InvalidInput(format!("worker inference '{name}' not registered")) }) } } @@ -2225,15 +2225,15 @@ fn llm_payload( } } -fn provider_payload( +fn worker_inference_payload( payload: Option, ) -> Result { match payload { - Some(nemo_relay_worker_proto::v1::invoke_request::Payload::Provider(value)) => { + Some(nemo_relay_worker_proto::v1::invoke_request::Payload::WorkerInference(value)) => { decode_json_envelope::(&value).map_err(Into::into) } _ => Err(WorkerSdkError::InvalidInput( - "expected inference provider payload".into(), + "expected worker inference payload".into(), )), } } @@ -2561,7 +2561,7 @@ fn all_surfaces() -> Vec { RegistrationSurface::MarkSanitizeGuardrail, RegistrationSurface::ScopeSanitizeStartGuardrail, RegistrationSurface::ScopeSanitizeEndGuardrail, - RegistrationSurface::InferenceProvider, + RegistrationSurface::WorkerInference, ] } diff --git a/crates/worker/tests/worker_sdk_tests.rs b/crates/worker/tests/worker_sdk_tests.rs index 462523b8b..7d95f2e49 100644 --- a/crates/worker/tests/worker_sdk_tests.rs +++ b/crates/worker/tests/worker_sdk_tests.rs @@ -117,7 +117,7 @@ async fn worker_service_enforces_auth_and_reports_registrations() { assert!( handshake .supported_surfaces - .contains(&(RegistrationSurface::InferenceProvider as i32)) + .contains(&(RegistrationSurface::WorkerInference as i32)) ); let bad_health = client @@ -278,7 +278,7 @@ async fn worker_service_enforces_auth_and_reports_registrations() { } #[tokio::test(flavor = "multi_thread")] -async fn worker_service_invokes_inference_provider() { +async fn worker_service_invokes_worker_inference() { let (handle, mut client) = spawn_worker( Arc::new(SurfacePlugin::default()), "http://127.0.0.1:9".into(), @@ -287,13 +287,13 @@ async fn worker_service_invokes_inference_provider() { let registrations = register_plugin(&mut client).await; assert!(registrations.iter().any(|registration| { registration.local_name == "local-model" - && registration.surface == RegistrationSurface::InferenceProvider as i32 + && registration.surface == RegistrationSurface::WorkerInference as i32 && registration.contract == "test.echo.v1" })); let response = invoke_json( &mut client, - provider_invoke("local-model", json!({"text": "private"})), + worker_inference_invoke("local-model", json!({"text": "private"})), ) .await; @@ -1259,8 +1259,8 @@ async fn worker_service_reports_missing_handlers_and_malformed_payloads() { "llm execution", ), ( - provider_invoke("missing-inference-provider", json!({})), - "inference provider", + worker_inference_invoke("missing-worker-inference", json!({})), + "worker inference", ), ] { assert_worker_error( @@ -1965,7 +1965,7 @@ impl WorkerPlugin for SurfacePlugin { ctx.register_llm_stream_execution_intercept("llm-stream-open-error", 1, |_, _, _| async { Err(WorkerSdkError::Callback("stream open boom".into())) }); - ctx.register_inference_provider("local-model", "test.echo.v1", |request| async move { + ctx.register_worker_inference("local-model", "test.echo.v1", |request| async move { Ok(set_json_field(request, "provider", "local-model")) }); Ok(()) @@ -2560,17 +2560,17 @@ fn tool_invoke( } } -fn provider_invoke(registration_name: &str, value: Json) -> InvokeRequest { +fn worker_inference_invoke(registration_name: &str, value: Json) -> InvokeRequest { InvokeRequest { activation_id: ACTIVATION_ID.into(), invocation_id: "invoke-1".into(), registration_name: registration_name.into(), - surface: RegistrationSurface::InferenceProvider as i32, + surface: RegistrationSurface::WorkerInference as i32, continuation_id: String::new(), scope: Some(scope_context()), auth_token: AUTH_TOKEN.into(), payload: Some( - nemo_relay_worker_proto::v1::invoke_request::Payload::Provider(json_env(value)), + nemo_relay_worker_proto::v1::invoke_request::Payload::WorkerInference(json_env(value)), ), } } diff --git a/docs/about-nemo-relay/release-notes/index.mdx b/docs/about-nemo-relay/release-notes/index.mdx index 55663a6a1..8390da450 100644 --- a/docs/about-nemo-relay/release-notes/index.mdx +++ b/docs/about-nemo-relay/release-notes/index.mdx @@ -97,8 +97,8 @@ For the new callback contract and codec operations, refer to availability, latency, and policy behavior. The local backend requires Python 3.11 or later and `nemoguardrails==0.22.0`. - The PII redaction plugin supports deterministic built-in policies and - worker-backed local-model providers. Local-model mode requires a separately - installed compatible `grpc-v1` provider; the optional Rampart provider + worker-backed local-model inference. Local-model mode requires a separately + installed compatible `grpc-v1` worker; the optional Rampart worker supports Latin-script text and does not provide complete PII coverage. - Pricing and optimization estimates depend on model names, token data, pricing sources, and freshness evidence. Missing or inconsistent evidence produces diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx index 85a703139..e754ed4c7 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx @@ -22,8 +22,8 @@ Workers implement the `PluginWorker` service: - `Handshake` and `Health` identify a ready worker. - `Validate` returns configuration diagnostics. -- `Register` returns declarative subscriber, guardrail, intercept, and - inference-provider registrations. +- `Register` returns declarative subscriber, guardrail, intercept, and worker + inference registrations. - `Invoke` and `InvokeStream` run registered behavior. - `CancelInvocation` requests cancellation, and `Shutdown` requests process termination. @@ -69,16 +69,16 @@ return `WorkerError` without registrations. The supported surfaces are: `LLM_EXECUTION_INTERCEPT`, and `LLM_STREAM_EXECUTION_INTERCEPT` - `MARK_SANITIZE_GUARDRAIL`, `SCOPE_SANITIZE_START_GUARDRAIL`, and `SCOPE_SANITIZE_END_GUARDRAIL` -- `INFERENCE_PROVIDER` +- `WORKER_INFERENCE` `InvokeRequest` identifies the registration, surface, invocation, optional continuation, and scope context. Its payload is one of an event, tool -invocation, LLM invocation, or component-owned provider request. +invocation, LLM invocation, or component-owned worker inference request. `InvokeResponse` returns an empty result, JSON result, guardrail result, LLM request-intercept result, tool-execution result, or `WorkerError`. -`INFERENCE_PROVIDER` declares a versioned contract and uses a JSON request and +`WORKER_INFERENCE` declares a versioned contract and uses a JSON request and JSON result. The consuming first-party component selects that exact contract, -owns its schema, and resolves the provider as `/`. +owns its schema, and resolves the callback as `/`. `InvokeStream` emits JSON chunks or `WorkerError` chunks. Every LLM sanitizer invocation includes a directional context with tagged codec diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx index 3c7451108..522326900 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx @@ -165,9 +165,9 @@ registers a tool request intercept that updates the request JSON and emits a mark through `PluginContext.runtime`. The `main` entrypoint blocks until Relay requests shutdown. -### Provide local inference +### Register worker inference -Use `register_inference_provider` when a first-party Relay component owns a +Use `register_worker_inference` when a first-party Relay component owns a versioned contract and needs an isolated detector or inference implementation: ```python @@ -178,7 +178,7 @@ async def detect(request: Json) -> Json: } -ctx.register_inference_provider( +ctx.register_worker_inference( "detector", "acme.pii_detection.v1", detect, diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx index cf687a433..6e25daecf 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx @@ -68,13 +68,13 @@ identity from `WorkerPlugin::plugin_id()`; custom launchers can use credentials and runs until Relay requests shutdown. Do not start the worker directly for normal operation. -### Provide local inference +### Register worker inference -Use `register_inference_provider` when a first-party Relay component owns a +Use `register_worker_inference` when a first-party Relay component owns a versioned contract and needs isolated inference: ```rust -context.register_inference_provider( +context.register_worker_inference( "detector", "acme.pii_detection.v1", |request| async move { diff --git a/docs/configure-plugins/pii-redaction/about.mdx b/docs/configure-plugins/pii-redaction/about.mdx index 3c6bd7f65..3ba06c983 100644 --- a/docs/configure-plugins/pii-redaction/about.mdx +++ b/docs/configure-plugins/pii-redaction/about.mdx @@ -129,18 +129,18 @@ incompatible raw provider shape. ## Current Boundaries This plugin is intentionally scoped to a deterministic built-in backend plus a -local worker-provider extension point. +local worker inference extension point. In particular: -- Local-model providers are optional dynamic plugins. They register a +- Local-model workers are optional dynamic plugins. They register a language-neutral JSON request-response contract through the `grpc-v1` worker protocol. - The plugin does not mutate the real callback arguments or return values. - `target_path_patterns` supports `*` as one complete JSON-pointer path segment. It does not support recursive or partial-segment matching. - `allow_network = false` prohibits network inference by contract, but the - worker process is not a network sandbox. Install only trusted providers. + worker process is not a network sandbox. Install only trusted workers. ## Pages diff --git a/docs/configure-plugins/pii-redaction/configuration.mdx b/docs/configure-plugins/pii-redaction/configuration.mdx index 5bb3e4a31..2a1688960 100644 --- a/docs/configure-plugins/pii-redaction/configuration.mdx +++ b/docs/configure-plugins/pii-redaction/configuration.mdx @@ -135,7 +135,7 @@ The following table compares the available PII redaction backends: | Managed `tool_output` | Supported | Supported | | Built-in actions | `remove`, `redact`, `regex_replace`, `hash`, `mask` | N/A | | Codec support | `openai_chat`, `openai_responses`, `anthropic_messages` | `openai_chat`, `openai_responses`, `anthropic_messages` | -| Runtime availability | Any runtime that includes the `nemo-relay-pii-redaction` plugin crate | Runtimes with an active `grpc-v1` worker provider | +| Runtime availability | Any runtime that includes the `nemo-relay-pii-redaction` plugin crate | Runtimes with active `grpc-v1` worker inference | ## Built-in Mode @@ -322,10 +322,10 @@ contextual PII. The worker performs inference only. The PII component selects fields, batches text, enforces deadlines, validates detections, applies confidence and label policy, replaces accepted spans, and fails closed. -The provider name is `/`. For example, a worker +The worker inference name is `/`. For example, a worker with plugin ID `acme.pii_worker` that registers `detector` for `nemo.relay.pii_detection.v1` is selected as `acme.pii_worker/detector`. Relay -rejects providers that declare another contract before installing the +rejects registrations that declare another contract before installing the sanitizer. ```toml @@ -354,17 +354,17 @@ allow_network = false max_latency_ms = 250 ``` -Worker providers are installed before built-in components initialize and are +Worker inference is installed before built-in components initialize and is removed after their dependent sanitizers. `allow_network = true` is rejected: this lane is for same-machine inference. This is a configuration contract, not a process sandbox. -Provider failures, timeouts, malformed responses, invalid UTF-8 spans, +Worker failures, timeouts, malformed responses, invalid UTF-8 spans, overlapping spans, and input-limit violations fail closed for the affected batch. If a configured codec cannot decode or safely re-encode an LLM payload, Relay omits that request or response payload from the emitted event. The default deadline is 250 ms for the complete selected payload, including every -provider batch. Configuration above 60 seconds is rejected. +inference batch. Configuration above 60 seconds is rejected. Use `profiles` to run deterministic recognizers before a contextual model: @@ -403,16 +403,16 @@ policy boundary. The generic local-model payload deadline defaults to 250 ms. Contextual models can need more time for large selected payloads. The Rampart profile above uses -5000 ms so a request near the 64 KiB provider limit has practical headroom on +5000 ms so a request near the 64 KiB detection-request limit has practical headroom on typical client hardware; benchmark representative inputs on deployment hardware before lowering it. -The optional Rampart provider is distributed as a manifest-backed Python source -bundle under `crates/pii-redaction/providers/rampart`. Prefetch its pinned model -before adding and enabling the worker: +The optional Rampart worker is included as a manifest-backed Python source +bundle under `crates/pii-redaction/workers/rampart`. Prefetch its pinned model +before adding and enabling it from a source checkout: ```bash -cd crates/pii-redaction/providers/rampart +cd crates/pii-redaction/workers/rampart uvx --from . nemo-relay-pii-rampart-prefetch nemo-relay plugins add ./relay-plugin.toml nemo-relay plugins enable nemo_relay.pii_rampart diff --git a/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py b/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py index e9d9060bb..fbdfd6a2c 100644 --- a/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py +++ b/examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py @@ -64,18 +64,7 @@ async def tag_tool_request(tool_name: str, args: Json) -> Json: ) return tagged_args - async def echo_local_model(request: Json) -> Json: - return { - "provider": "python_grpc_worker", - "request": request, - } - ctx.register_tool_request_intercept("tag_tool_request", tag_tool_request) - ctx.register_inference_provider( - "echo", - "examples.python_grpc_worker.echo.v1", - echo_local_model, - ) def _tag_json(value: Json, tag: str) -> Json: diff --git a/examples/python-grpc-worker-plugin/relay-plugin.toml b/examples/python-grpc-worker-plugin/relay-plugin.toml index b55df6679..c00d9ba80 100644 --- a/examples/python-grpc-worker-plugin/relay-plugin.toml +++ b/examples/python-grpc-worker-plugin/relay-plugin.toml @@ -22,7 +22,7 @@ manifest_root = "." artifact = "nemo_relay_python_grpc_worker_example/worker.py" [integrity] -sha256 = "sha256:f7313049118e931adfd7d1c9ab18dc5aefedf35dd01d5efea4dc12306ee08358" +sha256 = "sha256:966849be254cc6299a17a4bb65500363e9a48f98cc1e0091192e42b23821486f" [load] runtime = "python" diff --git a/justfile b/justfile index 937dec95d..bbbe2513e 100644 --- a/justfile +++ b/justfile @@ -971,9 +971,9 @@ check-python-worker-proto: } assert pb.SUBSCRIBER == 1 assert pb.LLM_STREAM_EXECUTION_INTERCEPT == 25 - assert pb.INFERENCE_PROVIDER == 40 + assert pb.WORKER_INFERENCE == 40 assert pb.Registration.DESCRIPTOR.fields_by_name["contract"].number == 5 - assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["provider"].number == 13 + assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["worker_inference"].number == 13 PY generate-worker-plugin-lockfile: diff --git a/python/plugin/README.md b/python/plugin/README.md index 3eac28c7a..3741e6fa1 100644 --- a/python/plugin/README.md +++ b/python/plugin/README.md @@ -26,7 +26,7 @@ protocol. - **Isolate plugin dependencies**: Run custom policy, middleware, or exporter code outside the Relay host process. - **Use the shared runtime contract**: Register subscribers, guardrails, and - intercepts or inference providers through `WorkerPlugin` and + intercepts or worker inference callbacks through `WorkerPlugin` and `PluginContext`. - **Call back into Relay safely**: Emit marks, create scopes, and continue managed execution through the host runtime handle. @@ -105,9 +105,9 @@ worker process. For a complete manifest and runnable plugin, see the [Python gRPC worker plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/python-grpc-worker-plugin/README.md). -## Inference Providers +## Worker Inference -Use `register_inference_provider` when a first-party Relay component owns a +Use `register_worker_inference` when a first-party Relay component owns a versioned request-response contract and needs isolated model inference: ```python @@ -118,14 +118,14 @@ async def detect(request: Json) -> Json: } -ctx.register_inference_provider( +ctx.register_worker_inference( "detector", "acme.pii_detection.v1", detect, ) ``` -Relay publishes this provider as `/detector`. The callback may be +Relay publishes this callback as `/detector`. The callback may be synchronous or asynchronous and should perform inference only. The consuming host component selects the exact contract and owns the payload schema, deadline, field traversal, output validation, and result application. diff --git a/python/plugin/src/nemo_relay_plugin/__init__.py b/python/plugin/src/nemo_relay_plugin/__init__.py index a3bb23893..cd1bd68eb 100644 --- a/python/plugin/src/nemo_relay_plugin/__init__.py +++ b/python/plugin/src/nemo_relay_plugin/__init__.py @@ -36,7 +36,7 @@ LlmOptimizationTokens: Explicit token evidence by category. LlmOptimizationTokenImpact: Baseline, effective, and saved token evidence. LlmRequestInterceptOutcome: Canonical LLM request-intercept result. - InferenceProviderCallback: Versioned inference-provider callback. + WorkerInferenceCallback: Versioned worker inference callback. ToolExecutionInterceptOutcome: Canonical tool execution-intercept result. DiagnosticLevel: Severity of a configuration diagnostic. ConfigDiagnostic: Structured configuration warning or error. @@ -76,7 +76,6 @@ Event, EventSanitizeCallback, EventSanitizeFields, - InferenceProviderCallback, Json, LlmCodecIdentity, LlmConditionalCallback, @@ -109,6 +108,7 @@ ToolNext, ToolRequestCallback, ToolSanitizeCallback, + WorkerInferenceCallback, WorkerPlugin, WorkerRequestCodec, WorkerResponseCodec, @@ -144,7 +144,7 @@ "LlmSanitizeResponseCallback", "LlmStreamNext", "LlmStreamExecutionCallback", - "InferenceProviderCallback", + "WorkerInferenceCallback", "PluginContext", "PluginRuntime", "PendingMarkSpec", diff --git a/python/plugin/src/nemo_relay_plugin/_api.py b/python/plugin/src/nemo_relay_plugin/_api.py index d14d390ff..cc32fb441 100644 --- a/python/plugin/src/nemo_relay_plugin/_api.py +++ b/python/plugin/src/nemo_relay_plugin/_api.py @@ -845,7 +845,7 @@ def register(self, ctx: PluginContext, config: Json) -> None | Awaitable[None]: [str, LlmRequest, "LlmStreamNext"], Iterable[Json] | AsyncIterator[Json] | Awaitable[Iterable[Json] | AsyncIterator[Json]], ] -InferenceProviderCallback: TypeAlias = Callable[[Json], Json | Awaitable[Json]] +WorkerInferenceCallback: TypeAlias = Callable[[Json], Json | Awaitable[Json]] @dataclass(slots=True) @@ -866,7 +866,7 @@ class _Handlers: llm_requests: dict[str, LlmRequestCallback] llm_executions: dict[str, LlmExecutionCallback] llm_stream_executions: dict[str, LlmStreamExecutionCallback] - inference_providers: dict[str, InferenceProviderCallback] + worker_inference: dict[str, WorkerInferenceCallback] @classmethod def empty(cls) -> _Handlers: @@ -887,7 +887,7 @@ def empty(cls) -> _Handlers: llm_requests={}, llm_executions={}, llm_stream_executions={}, - inference_providers={}, + worker_inference={}, ) @@ -956,27 +956,27 @@ def register_subscriber(self, name: str, callback: SubscriberCallback) -> None: self._push_registration(name, pb.SUBSCRIBER, 0, False) self._handlers.subscribers[name] = callback - def register_inference_provider( + def register_worker_inference( self, name: str, contract: str, - callback: InferenceProviderCallback, + callback: WorkerInferenceCallback, ) -> None: - """Register a named inference provider for a versioned host contract. + """Register named worker inference for a versioned host contract. Args: - name: Stable provider name selected by a consuming host component. - contract: Versioned request-response contract implemented by the provider. + name: Stable inference name selected by a consuming host component. + contract: Versioned request-response contract implemented by the worker. callback: Function receiving and returning component-owned JSON. The callback can return a value directly or through an awaitable. - Provider boundary: - Providers perform model inference only. The consuming host + Ownership boundary: + Workers perform model inference only. The consuming host component owns field selection, policy, and output application. """ - self._push_registration(name, pb.INFERENCE_PROVIDER, 0, False, contract=contract) - self._handlers.inference_providers[name] = callback + self._push_registration(name, pb.WORKER_INFERENCE, 0, False, contract=contract) + self._handlers.worker_inference[name] = callback def _register_event_sanitizer( self, @@ -2094,15 +2094,15 @@ async def _invoke_result(self, request: Any) -> Any: ), ) ) - if request.surface == pb.INFERENCE_PROVIDER: + if request.surface == pb.WORKER_INFERENCE: result = await _maybe_await( self._handler( - self._handlers.inference_providers, + self._handlers.worker_inference, request.registration_name, )( _decode_required_envelope( - request.provider, - "inference provider request", + request.worker_inference, + "worker inference request", ) ) ) @@ -2242,7 +2242,7 @@ def _all_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, - pb.INFERENCE_PROVIDER, + pb.WORKER_INFERENCE, ] diff --git a/python/tests/plugin/test_public_api_docstrings.py b/python/tests/plugin/test_public_api_docstrings.py index 4ab3074c8..7b77ccbde 100644 --- a/python/tests/plugin/test_public_api_docstrings.py +++ b/python/tests/plugin/test_public_api_docstrings.py @@ -35,7 +35,7 @@ "LlmRequestCallback", "LlmExecutionCallback", "LlmStreamExecutionCallback", - "InferenceProviderCallback", + "WorkerInferenceCallback", } diff --git a/python/tests/plugin/test_worker_sdk.py b/python/tests/plugin/test_worker_sdk.py index 20062585d..09be852a3 100644 --- a/python/tests/plugin/test_worker_sdk.py +++ b/python/tests/plugin/test_worker_sdk.py @@ -363,7 +363,7 @@ async def llm_stream_execution(name: str, request: Json, next_call: Any) -> Asyn async for chunk in stream: yield _tag(chunk, "llm_stream_execution") - async def inference_provider(request: Json) -> Json: + async def worker_inference(request: Json) -> Json: return _tag(request, "local_model") ctx.register_subscriber("subscriber", subscriber) @@ -381,10 +381,10 @@ async def inference_provider(request: Json) -> Json: ctx.register_llm_request_intercept("llm_request", llm_request, priority=9, break_chain=True) ctx.register_llm_execution_intercept("llm_execution", llm_execution, priority=10) ctx.register_llm_stream_execution_intercept("llm_stream_execution", llm_stream_execution, priority=11) - ctx.register_inference_provider( + ctx.register_worker_inference( "local_model", "test.echo.v1", - inference_provider, + worker_inference, ) @@ -419,9 +419,9 @@ def test_generated_proto_matches_worker_contract(): assert pb.MARK_SANITIZE_GUARDRAIL == 30 assert pb.SCOPE_SANITIZE_START_GUARDRAIL == 31 assert pb.SCOPE_SANITIZE_END_GUARDRAIL == 32 - assert pb.INFERENCE_PROVIDER == 40 + assert pb.WORKER_INFERENCE == 40 assert pb.Registration.DESCRIPTOR.fields_by_name["contract"].number == 5 - assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["provider"].number == 13 + assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["worker_inference"].number == 13 assert pb.CUSTOM == 10 @@ -480,7 +480,7 @@ async def test_health_handshake_validate_register_and_all_surfaces(service: _Wor ("llm_request", pb.LLM_REQUEST_INTERCEPT, 9, True, ""), ("llm_execution", pb.LLM_EXECUTION_INTERCEPT, 10, False, ""), ("llm_stream_execution", pb.LLM_STREAM_EXECUTION_INTERCEPT, 11, False, ""), - ("local_model", pb.INFERENCE_PROVIDER, 0, False, "test.echo.v1"), + ("local_model", pb.WORKER_INFERENCE, 0, False, "test.echo.v1"), ] @@ -1457,7 +1457,7 @@ async def test_unary_invoke_success_paths(service: _WorkerService, host_stub: Re assert llm_execution["next_llm"]["content"]["llm_execute_gpt-test"] local_model = await service.Invoke( - _provider_request("local_model", {"text": "private"}), + _worker_inference_request("local_model", {"text": "private"}), AbortContext(), ) assert local_model.WhichOneof("result") == "json" @@ -1481,7 +1481,7 @@ async def test_unary_invoke_failure_paths(service: _WorkerService): assert "not registered" in missing_handler.error.message missing_provider = await service.Invoke( - _provider_request("missing", {}), + _worker_inference_request("missing", {}), AbortContext(), ) assert missing_provider.WhichOneof("result") == "error" @@ -2773,12 +2773,12 @@ def _tool_request(registration_name: str, surface: int, value: Json) -> Any: ) -def _provider_request(registration_name: str, value: Json) -> Any: +def _worker_inference_request(registration_name: str, value: Json) -> Any: return _invoke_request( registration_name, - pb.INFERENCE_PROVIDER, + pb.WORKER_INFERENCE, continuation_id="", - provider=_json_envelope(JSON_SCHEMA, value), + worker_inference=_json_envelope(JSON_SCHEMA, value), ) @@ -2870,5 +2870,5 @@ def _all_expected_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, - pb.INFERENCE_PROVIDER, + pb.WORKER_INFERENCE, ] From ae28eee3257a6843affb71c9870ae829635ad4df Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 27 Jul 2026 12:21:49 -0700 Subject: [PATCH 10/83] refactor(pii): remove worker inference design Signed-off-by: Alex Fournier --- crates/cli/src/server/mod.rs | 7 +- crates/core/src/lib.rs | 3 - crates/core/src/plugin.rs | 110 +- crates/core/src/plugin/dynamic/host.rs | 21 +- crates/core/src/plugin/dynamic/worker.rs | 151 +-- crates/core/src/plugin/worker_inference.rs | 213 ---- .../tests/fixtures/worker_plugin/src/main.rs | 66 -- .../tests/integration/worker_plugin_tests.rs | 208 +--- .../core/tests/unit/dynamic_worker_tests.rs | 42 - crates/core/tests/unit/plugin_tests.rs | 98 -- .../core/tests/unit/worker_inference_tests.rs | 116 -- crates/node/pii_redaction.d.ts | 18 +- crates/node/pii_redaction.js | 18 +- crates/node/tests/pii_redaction_tests.mjs | 54 - crates/pii-redaction/Cargo.toml | 2 +- crates/pii-redaction/README.md | 158 +-- crates/pii-redaction/src/builtin.rs | 11 +- crates/pii-redaction/src/component.rs | 190 +-- crates/pii-redaction/src/local.rs | 1051 ++--------------- .../tests/unit/component_tests.rs | 400 +------ .../pii-redaction/tests/unit/local_tests.rs | 971 --------------- .../tests/worker_detection_tests.rs | 360 ------ .../pii-redaction/workers/rampart/MANIFEST.in | 5 - .../pii-redaction/workers/rampart/README.md | 161 --- .../workers/rampart/THIRD_PARTY_NOTICES.md | 18 - .../workers/rampart/config.schema.json | 54 - .../nemo_relay_pii_rampart/__init__.py | 13 - .../nemo_relay_pii_rampart/detector.py | 572 --------- .../nemo_relay_pii_rampart/prefetch.py | 27 - .../rampart/nemo_relay_pii_rampart/py.typed | 0 .../rampart/nemo_relay_pii_rampart/worker.py | 116 -- .../workers/rampart/pyproject.toml | 57 - .../workers/rampart/relay-plugin.toml | 32 - .../workers/rampart/tests/test_detector.py | 339 ------ .../workers/rampart/tests/test_worker.py | 173 --- crates/worker-proto/README.md | 5 - .../nemo/relay/worker/v1/plugin_worker.proto | 4 +- crates/worker-proto/tests/proto_tests.rs | 26 +- crates/worker/README.md | 26 +- crates/worker/src/lib.rs | 67 -- crates/worker/tests/worker_sdk_tests.rs | 56 +- docs/about-nemo-relay/release-notes/index.mdx | 6 +- .../grpc-worker/grpc-worker-protocol.mdx | 17 +- .../grpc-worker/python/about.mdx | 25 - .../grpc-worker/rust/about.mdx | 23 - .../configure-plugins/pii-redaction/about.mdx | 29 +- .../pii-redaction/configuration.mdx | 153 +-- go/nemo_relay/pii_redaction.go | 57 +- go/nemo_relay/pii_redaction/pii_redaction.go | 10 +- .../pii_redaction/pii_redaction_test.go | 32 +- go/nemo_relay/pii_redaction_test.go | 36 +- justfile | 3 - python/nemo_relay/pii_redaction.py | 48 +- python/nemo_relay/pii_redaction.pyi | 15 - python/plugin/README.md | 28 +- .../plugin/src/nemo_relay_plugin/__init__.py | 3 - python/plugin/src/nemo_relay_plugin/_api.py | 50 +- .../plugin/test_public_api_docstrings.py | 1 - python/tests/plugin/test_worker_sdk.py | 77 +- python/tests/test_pii_redaction_plugin.py | 54 - 60 files changed, 312 insertions(+), 6374 deletions(-) delete mode 100644 crates/core/src/plugin/worker_inference.rs delete mode 100644 crates/core/tests/unit/worker_inference_tests.rs delete mode 100644 crates/pii-redaction/tests/unit/local_tests.rs delete mode 100644 crates/pii-redaction/tests/worker_detection_tests.rs delete mode 100644 crates/pii-redaction/workers/rampart/MANIFEST.in delete mode 100644 crates/pii-redaction/workers/rampart/README.md delete mode 100644 crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md delete mode 100644 crates/pii-redaction/workers/rampart/config.schema.json delete mode 100644 crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py delete mode 100644 crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py delete mode 100644 crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/prefetch.py delete mode 100644 crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/py.typed delete mode 100644 crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py delete mode 100644 crates/pii-redaction/workers/rampart/pyproject.toml delete mode 100644 crates/pii-redaction/workers/rampart/relay-plugin.toml delete mode 100644 crates/pii-redaction/workers/rampart/tests/test_detector.py delete mode 100644 crates/pii-redaction/workers/rampart/tests/test_worker.py diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index 87a1b2e40..a263f0a54 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -25,7 +25,6 @@ use nemo_relay::plugin::dynamic::{ }; use nemo_relay::plugin::{ PluginComponentSpec, PluginConfig, clear_plugin_configuration, initialize_plugins_exact, - initialize_plugins_exact_with_worker_inference, }; use nemo_relay_adaptive::plugin_component::register_adaptive_component; use nemo_relay_pii_redaction::component::register_pii_redaction_component; @@ -1117,11 +1116,7 @@ impl PluginActivation { CliError::Config(format!("worker plugin load failed: {error}")) })?) }; - let worker_inference = worker - .as_ref() - .map(WorkerPluginActivation::worker_inference_registry) - .unwrap_or_default(); - initialize_plugins_exact_with_worker_inference(plugin_config, worker_inference) + initialize_plugins_exact(plugin_config) .await .map_err(|error| CliError::Config(format!("plugin activation failed: {error}")))?; Ok(Self { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 6c805258d..18c9f5df1 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -72,6 +72,3 @@ pub mod stream; #[cfg(test)] #[path = "../tests/unit/types_tests.rs"] mod types_tests; -#[cfg(test)] -#[path = "../tests/unit/worker_inference_tests.rs"] -mod worker_inference_tests; diff --git a/crates/core/src/plugin.rs b/crates/core/src/plugin.rs index d3133edc7..ff7bc6c79 100644 --- a/crates/core/src/plugin.rs +++ b/crates/core/src/plugin.rs @@ -48,12 +48,6 @@ pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel}; pub mod dynamic; pub use dynamic::*; -mod worker_inference; -#[doc(hidden)] -pub use worker_inference::{ - WorkerInference, WorkerInferenceDescriptor, WorkerInferenceFn, WorkerInferenceRegistration, - WorkerInferenceRegistry, -}; type PluginMap = HashMap; @@ -348,7 +342,6 @@ impl PluginRegistration { pub struct PluginRegistrationContext { registrations: Vec, namespace: Option, - worker_inference: WorkerInferenceRegistry, } impl PluginRegistrationContext { @@ -362,29 +355,9 @@ impl PluginRegistrationContext { Self { registrations: vec![], namespace: Some(namespace.into()), - worker_inference: WorkerInferenceRegistry::default(), - } - } - - /// Creates a registration context backed by host-owned worker inference. - #[doc(hidden)] - pub fn with_worker_inference( - namespace: Option, - worker_inference: WorkerInferenceRegistry, - ) -> Self { - Self { - registrations: Vec::new(), - namespace, - worker_inference, } } - /// Resolves worker inference implementing the required contract. - #[doc(hidden)] - pub fn worker_inference(&self, name: &str, expected_contract: &str) -> Result { - self.worker_inference.resolve(name, expected_contract) - } - /// Returns the runtime-qualified name for a plugin-local registration. /// /// Plugin handlers should pass stable component-local names such as @@ -1336,22 +1309,12 @@ pub fn plugin_config_schema() -> Json { /// is removed before the new configuration is activated. #[doc(hidden)] pub async fn initialize_plugins_exact(config: PluginConfig) -> Result { - initialize_plugins_exact_with_worker_inference(config, WorkerInferenceRegistry::default()).await -} - -/// Configures plugin components with host-owned worker inference. -#[doc(hidden)] -pub async fn initialize_plugins_exact_with_worker_inference( - config: PluginConfig, - worker_inference: WorkerInferenceRegistry, -) -> Result { run_owned_plugin_mutation("plugin initialization", move || async move { let lease = LegacyPluginMutationLease::acquire()?; let rollback_failures = Arc::new(Mutex::new(Vec::new())); let initialization = tokio::spawn(initialize_plugins_exact_inner( config, Some(Arc::clone(&rollback_failures)), - worker_inference, )) .await .map_err(|error| { @@ -1468,16 +1431,14 @@ pub(crate) async fn initialize_plugins_exact_for_host( config: PluginConfig, owner_id: u64, rollback_failures: Arc>>, - worker_inference: WorkerInferenceRegistry, ) -> Result { verify_plugin_host_owner(owner_id)?; - initialize_plugins_exact_inner(config, Some(rollback_failures), worker_inference).await + initialize_plugins_exact_inner(config, Some(rollback_failures)).await } async fn initialize_plugins_exact_inner( config: PluginConfig, rollback_failures: Option>>>, - worker_inference: WorkerInferenceRegistry, ) -> Result { let enabled_component_count = config .components @@ -1514,17 +1475,11 @@ async fn initialize_plugins_exact_inner( match initialize_plugin_components_catching_panics( config.clone(), rollback_failures.clone(), - worker_inference.clone(), ) .await { Ok(registrations) => { - store_active_plugin_configuration( - config, - report.clone(), - registrations, - worker_inference, - )?; + store_active_plugin_configuration(config, report.clone(), registrations)?; log::info!( target: "nemo_relay.plugin", event = "plugin_configuration_replaced", @@ -1536,7 +1491,6 @@ async fn initialize_plugins_exact_inner( Err(err) => match initialize_plugin_components_catching_panics( previous_state.config.clone(), rollback_failures.clone(), - previous_state.worker_inference.clone(), ) .await { @@ -1546,7 +1500,6 @@ async fn initialize_plugins_exact_inner( previous_state.config, previous_report, registrations, - previous_state.worker_inference, )?; log::warn!( target: "nemo_relay.plugin", @@ -1570,13 +1523,9 @@ async fn initialize_plugins_exact_inner( }, } } else { - let registrations = initialize_plugin_components_catching_panics( - config.clone(), - rollback_failures, - worker_inference.clone(), - ) - .await?; - store_active_plugin_configuration(config, report.clone(), registrations, worker_inference)?; + let registrations = + initialize_plugin_components_catching_panics(config.clone(), rollback_failures).await?; + store_active_plugin_configuration(config, report.clone(), registrations)?; log::info!( target: "nemo_relay.plugin", event = "plugin_configuration_activated", @@ -1590,17 +1539,14 @@ async fn initialize_plugins_exact_inner( async fn initialize_plugin_components_catching_panics( config: PluginConfig, rollback_failures: Option>>>, - worker_inference: WorkerInferenceRegistry, ) -> Result> { - tokio::spawn(async move { - initialize_plugin_components(&config, rollback_failures, worker_inference).await - }) - .await - .map_err(|error| { - PluginError::Internal(format!( - "plugin component initialization task failed: {error}" - )) - })? + tokio::spawn(async move { initialize_plugin_components(&config, rollback_failures).await }) + .await + .map_err(|error| { + PluginError::Internal(format!( + "plugin component initialization task failed: {error}" + )) + })? } /// Validates and activates `config` layered on top of the discovered @@ -1613,16 +1559,6 @@ pub async fn initialize_plugins(config: PluginConfig) -> Result { initialize_plugins_exact(config).await } -/// Resolves discovered configuration and activates it with host-owned worker inference. -#[doc(hidden)] -pub async fn initialize_plugins_with_worker_inference( - config: PluginConfig, - worker_inference: WorkerInferenceRegistry, -) -> Result { - let config = resolve_plugin_config(config)?; - initialize_plugins_exact_with_worker_inference(config, worker_inference).await -} - /// Layers `config` over the default discovered `plugins.toml` files. /// /// This is crate-visible so owned dynamic-plugin activation can use the same @@ -2019,13 +1955,11 @@ struct ActivePluginConfiguration { config: PluginConfig, report: ConfigReport, registrations: Vec, - worker_inference: WorkerInferenceRegistry, } async fn initialize_plugin_components( config: &PluginConfig, rollback_failures: Option>>>, - worker_inference: WorkerInferenceRegistry, ) -> Result> { ensure_builtin_plugins_registered()?; let totals = plugin_component_totals(config); @@ -2054,11 +1988,8 @@ async fn initialize_plugin_components( totals.get(component.kind.as_str()).copied().unwrap_or(1), ); - let mut pending = PendingPluginRegistrationContext::new( - namespace, - rollback_failures.clone(), - worker_inference.clone(), - ); + let mut pending = + PendingPluginRegistrationContext::new(namespace, rollback_failures.clone()); plugin .register(&component.config, &mut pending.context) .await?; @@ -2103,16 +2034,9 @@ struct PendingPluginRegistrationContext { } impl PendingPluginRegistrationContext { - fn new( - namespace: String, - rollback_failures: Option>>>, - worker_inference: WorkerInferenceRegistry, - ) -> Self { + fn new(namespace: String, rollback_failures: Option>>>) -> Self { Self { - context: PluginRegistrationContext::with_worker_inference( - Some(namespace), - worker_inference, - ), + context: PluginRegistrationContext::with_namespace(namespace), rollback_failures, } } @@ -2147,7 +2071,6 @@ fn store_active_plugin_configuration( config: PluginConfig, report: ConfigReport, registrations: Vec, - worker_inference: WorkerInferenceRegistry, ) -> Result<()> { let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| { PluginError::Internal(format!("active plugin configuration lock poisoned: {err}")) @@ -2156,7 +2079,6 @@ fn store_active_plugin_configuration( config, report, registrations, - worker_inference, }); Ok(()) } diff --git a/crates/core/src/plugin/dynamic/host.rs b/crates/core/src/plugin/dynamic/host.rs index 07f3c5b3d..8b3f3e2df 100644 --- a/crates/core/src/plugin/dynamic/host.rs +++ b/crates/core/src/plugin/dynamic/host.rs @@ -17,7 +17,7 @@ use serde_json::{Map, Value as Json}; use crate::plugin::{ ConfigReport, PluginComponentSpec, PluginConfig, PluginHostLease, Result, - WorkerInferenceRegistry, acquire_plugin_host_lease, clear_plugin_configuration_for_host, + acquire_plugin_host_lease, clear_plugin_configuration_for_host, ensure_builtin_plugins_registered, initialize_plugins_exact_for_host, resolve_plugin_config, run_owned_plugin_mutation, }; @@ -186,18 +186,10 @@ impl PluginHostActivation { ); let rollback_failures = Arc::new(Mutex::new(Vec::new())); let owner_id = claim.owner_id(); - #[cfg(feature = "worker-grpc")] - let worker_inference = worker - .as_ref() - .map(WorkerPluginActivation::worker_inference_registry) - .unwrap_or_else(WorkerInferenceRegistry::default); - #[cfg(not(feature = "worker-grpc"))] - let worker_inference = WorkerInferenceRegistry::default(); let initialization = tokio::spawn(initialize_plugins_exact_for_host( config, owner_id, Arc::clone(&rollback_failures), - worker_inference, )) .await .map_err(|error| { @@ -270,16 +262,6 @@ impl PluginHostActivation { self.active } - /// Returns the worker inference registry owned by this activation. - #[doc(hidden)] - pub fn worker_inference_registry(&self) -> WorkerInferenceRegistry { - #[cfg(feature = "worker-grpc")] - if let Some(worker) = &self.worker { - return worker.worker_inference_registry(); - } - WorkerInferenceRegistry::default() - } - /// Clear registered callbacks before unloading libraries and workers. pub fn clear(mut self) -> Result<()> { self.clear_inner() @@ -318,7 +300,6 @@ impl PluginHostActivation { #[cfg(feature = "worker-grpc")] if let Some(worker) = &mut self.worker { runtime_outcome.merge(worker.deregister_plugin_kinds_checked()); - runtime_outcome.merge(worker.deregister_worker_inference_checked()); } // A worker cannot be stopped while its registry adapter might still be diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 62273f8df..79e2e70ce 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -72,9 +72,8 @@ use crate::codec::request::{ANNOTATED_LLM_REQUEST_SCHEMA, AnnotatedLlmRequest}; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::error::{FlowError, Result as FlowResult}; use crate::plugin::{ - ConfigDiagnostic, DiagnosticLevel, Plugin, PluginDeregistrationOutcome, PluginError, - PluginRegistrationContext, WorkerInferenceDescriptor, WorkerInferenceRegistration, - WorkerInferenceRegistry, deregister_plugin_registration_checked, register_plugin_tracked, + ConfigDiagnostic, DiagnosticLevel, Plugin, PluginError, PluginRegistrationContext, + deregister_plugin_registration_checked, register_plugin_tracked, }; use super::{ @@ -130,8 +129,6 @@ pub struct WorkerPluginLoadSpec { pub struct WorkerPluginActivation { plugins: Vec>, plugin_registrations: Vec<(String, u64)>, - worker_inference: WorkerInferenceRegistry, - worker_inference_registrations: Vec, } impl WorkerPluginActivation { @@ -143,20 +140,10 @@ impl WorkerPluginActivation { /// Consumes the activation; deregistration runs from `Drop`. pub fn clear(self) {} - /// Returns the host-owned worker inference registry installed by this activation. - #[doc(hidden)] - pub fn worker_inference_registry(&self) -> WorkerInferenceRegistry { - self.worker_inference.clone() - } - pub(crate) fn deregister_plugin_kinds_checked(&mut self) -> DynamicPluginTeardownOutcome { deregister_tracked_registrations_checked(&mut self.plugin_registrations, "worker") } - pub(crate) fn deregister_worker_inference_checked(&mut self) -> DynamicPluginTeardownOutcome { - deregister_worker_inference_checked(&mut self.worker_inference_registrations) - } - pub(crate) fn shutdown_plugins_checked(&self) -> DynamicPluginTeardownOutcome { let mut outcome = DynamicPluginTeardownOutcome::success(); for plugin in self.plugins.iter().rev() { @@ -168,7 +155,6 @@ impl WorkerPluginActivation { impl Drop for WorkerPluginActivation { fn drop(&mut self) { - let _ = deregister_worker_inference_checked(&mut self.worker_inference_registrations); for (plugin_kind, registration_id) in self.plugin_registrations.iter().rev() { let _ = deregister_plugin_registration_checked(plugin_kind, *registration_id); } @@ -180,43 +166,22 @@ impl Drop for WorkerPluginActivation { /// The returned activation must be kept alive until after active plugin /// configuration has been cleared. pub fn load_worker_plugins(specs: I) -> crate::plugin::Result -where - I: IntoIterator, -{ - load_worker_plugins_with_worker_inference(specs, WorkerInferenceRegistry::default()) -} - -/// Loads worker plugins into an existing host-owned worker inference registry. -#[doc(hidden)] -pub fn load_worker_plugins_with_worker_inference( - specs: I, - worker_inference: WorkerInferenceRegistry, -) -> crate::plugin::Result where I: IntoIterator, { let mut activation = WorkerPluginActivation { plugins: Vec::new(), plugin_registrations: Vec::new(), - worker_inference: worker_inference.clone(), - worker_inference_registrations: Vec::new(), }; for spec in specs { let instance = load_one_worker_plugin(&spec)?; - let worker_inference_registrations = - instance.install_worker_inference(&worker_inference)?; let plugin_kind = instance.plugin_kind.clone(); - // Transfer ownership before the next fallible registration so Drop can - // unwind worker inference and the worker process on partial activation. - activation.plugins.push(instance.clone()); - activation - .worker_inference_registrations - .extend(worker_inference_registrations); let registration_id = register_plugin_tracked(Arc::new(WorkerPluginAdapter { plugin_kind: plugin_kind.clone(), allows_multiple_components: instance.allows_multiple_components, instance: instance.clone(), }))?; + activation.plugins.push(instance); activation .plugin_registrations .push((plugin_kind, registration_id)); @@ -1078,42 +1043,6 @@ fn clear_host_python_environment(command: &mut Command) { } impl WorkerPluginInstance { - fn install_worker_inference( - &self, - registry: &WorkerInferenceRegistry, - ) -> crate::plugin::Result> { - let mut registrations = Vec::new(); - for registration in &self.registrations { - let surface = RegistrationSurface::try_from(registration.surface).map_err(|_| { - PluginError::RegistrationFailed(format!( - "worker plugin '{}' returned unsupported registration surface {}", - self.plugin_kind, registration.surface - )) - })?; - if surface != RegistrationSurface::WorkerInference { - continue; - } - let callback_name = registration.local_name.clone(); - let inference_name = format!("{}/{}", self.plugin_kind, callback_name); - let callback = self.clone_for_callback(); - let descriptor = - WorkerInferenceDescriptor::new(inference_name, registration.contract.clone())?; - match registry.register( - descriptor, - Arc::new(move |request, timeout| { - callback.invoke_worker_inference(&callback_name, request, timeout) - }), - ) { - Ok(registration) => registrations.push(registration), - Err(error) => { - let _ = deregister_worker_inference_checked(&mut registrations); - return Err(error); - } - } - } - Ok(registrations) - } - fn install_registrations( &self, ctx: &mut PluginRegistrationContext, @@ -1153,10 +1082,6 @@ impl WorkerPluginInstance { | RegistrationSurface::LlmStreamExecutionIntercept => { self.install_llm_registration(ctx, registration, surface)? } - RegistrationSurface::WorkerInference => { - // Worker inference is installed during host bootstrap so components - // can resolve their contracts before runtime callbacks register. - } RegistrationSurface::Unspecified => { return Err(PluginError::RegistrationFailed(format!( "worker plugin '{}' returned unspecified registration surface", @@ -1444,36 +1369,6 @@ struct WorkerPluginCallback { } impl WorkerPluginCallback { - fn invoke_worker_inference( - &self, - registration_name: &str, - value: Json, - timeout: Duration, - ) -> crate::plugin::Result { - let request = self.base_request( - registration_name, - RegistrationSurface::WorkerInference, - None, - Some(invoke_request_payload::Payload::WorkerInference( - json_envelope_infallible(JSON_SCHEMA, &value), - )), - ); - let response = block_on_handle( - &self.runtime, - self.invoke_async_with_timeout(request, timeout), - ) - .map_err(|error| { - PluginError::RegistrationFailed(format!( - "worker inference '{registration_name}' invocation failed: {error}" - )) - })?; - json_from_invoke_response(response).map_err(|error| { - PluginError::RegistrationFailed(format!( - "worker inference '{registration_name}' returned an invalid response: {error}" - )) - }) - } - fn log_callback_fallback(&self, callback_name: &str, surface: RegistrationSurface) { log::warn!( target: "nemo_relay.worker", @@ -1486,33 +1381,6 @@ impl WorkerPluginCallback { } } -fn deregister_worker_inference_checked( - registrations: &mut Vec, -) -> DynamicPluginTeardownOutcome { - let mut outcome = DynamicPluginTeardownOutcome::success(); - for mut registration in std::mem::take(registrations).into_iter().rev() { - let name = registration.name().to_string(); - match registration.deregister_checked() { - Ok(PluginDeregistrationOutcome::Removed) => {} - Ok(PluginDeregistrationOutcome::Missing) => outcome.record_error( - format!("worker inference '{name}' was not registered during teardown"), - true, - ), - Ok(PluginDeregistrationOutcome::Replaced) => outcome.record_error( - format!( - "worker inference '{name}' was replaced during teardown and was left registered" - ), - true, - ), - Err(error) => outcome.record_error( - format!("failed to deregister worker inference '{name}': {error}"), - false, - ), - } - } - outcome -} - struct WorkerInvocationGuard { runtime: tokio::runtime::Handle, client: PluginWorkerClient, @@ -3080,19 +2948,6 @@ fn validate_registration_plan( "worker plugin '{plugin_id}' returned unspecified registration surface" ))); } - let contract = registration.contract.trim(); - if surface == RegistrationSurface::WorkerInference && contract.is_empty() { - return Err(PluginError::RegistrationFailed(format!( - "worker plugin '{plugin_id}' returned worker inference '{}' without a contract", - registration.local_name - ))); - } - if surface != RegistrationSurface::WorkerInference && !contract.is_empty() { - return Err(PluginError::RegistrationFailed(format!( - "worker plugin '{plugin_id}' returned a contract for non-inference registration '{}'", - registration.local_name - ))); - } } Ok(()) } diff --git a/crates/core/src/plugin/worker_inference.rs b/crates/core/src/plugin/worker_inference.rs deleted file mode 100644 index 85d893cc0..000000000 --- a/crates/core/src/plugin/worker_inference.rs +++ /dev/null @@ -1,213 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Host-owned worker inference used by plugin components. - -use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -use serde_json::Value as Json; - -use super::{PluginDeregistrationOutcome, PluginError, Result}; - -/// Versioned JSON request-response callback implemented by a worker. -#[doc(hidden)] -pub type WorkerInferenceFn = Arc Result + Send + Sync + 'static>; - -/// Stable identity and request-response contract for one worker inference callback. -#[doc(hidden)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WorkerInferenceDescriptor { - name: String, - contract: String, -} - -impl WorkerInferenceDescriptor { - /// Creates a descriptor after validating its stable identifiers. - pub fn new(name: impl Into, contract: impl Into) -> Result { - let name = normalized_identifier(name.into(), "worker inference name")?; - let contract = normalized_identifier(contract.into(), "worker inference contract")?; - Ok(Self { name, contract }) - } - - /// Returns the host-qualified worker inference name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the versioned request-response contract identifier. - pub fn contract(&self) -> &str { - &self.contract - } -} - -/// Resolved worker inference callback whose contract has already been checked. -#[doc(hidden)] -#[derive(Clone)] -pub struct WorkerInference { - descriptor: WorkerInferenceDescriptor, - callback: WorkerInferenceFn, -} - -impl WorkerInference { - /// Returns the worker inference descriptor. - pub fn descriptor(&self) -> &WorkerInferenceDescriptor { - &self.descriptor - } - - /// Invokes the worker with the component-owned request and deadline. - pub fn invoke(&self, request: Json, timeout: Duration) -> Result { - (self.callback)(request, timeout) - } -} - -struct RegisteredWorkerInference { - registration_id: u64, - descriptor: WorkerInferenceDescriptor, - callback: WorkerInferenceFn, -} - -struct WorkerInferenceRegistryInner { - entries: RwLock>, - next_registration_id: AtomicU64, -} - -/// Host-scoped registry for versioned worker inference callbacks. -#[doc(hidden)] -#[derive(Clone)] -pub struct WorkerInferenceRegistry { - inner: Arc, -} - -impl Default for WorkerInferenceRegistry { - fn default() -> Self { - Self { - inner: Arc::new(WorkerInferenceRegistryInner { - entries: RwLock::new(HashMap::new()), - next_registration_id: AtomicU64::new(1), - }), - } - } -} - -impl WorkerInferenceRegistry { - /// Registers worker inference and returns an ownership handle. - pub fn register( - &self, - descriptor: WorkerInferenceDescriptor, - callback: WorkerInferenceFn, - ) -> Result { - let mut entries = self.inner.entries.write().map_err(|error| { - PluginError::Internal(format!("worker inference registry lock poisoned: {error}")) - })?; - if entries.contains_key(descriptor.name()) { - return Err(PluginError::RegistrationFailed(format!( - "worker inference '{}' is already registered", - descriptor.name() - ))); - } - let registration_id = self - .inner - .next_registration_id - .fetch_add(1, Ordering::Relaxed); - let name = descriptor.name().to_string(); - entries.insert( - name.clone(), - RegisteredWorkerInference { - registration_id, - descriptor, - callback, - }, - ); - Ok(WorkerInferenceRegistration { - registry: self.clone(), - name, - registration_id: Some(registration_id), - }) - } - - /// Resolves worker inference only when its declared contract exactly matches. - pub fn resolve(&self, name: &str, expected_contract: &str) -> Result { - let name = normalized_identifier(name.to_string(), "worker inference name")?; - let expected_contract = - normalized_identifier(expected_contract.to_string(), "worker inference contract")?; - let entries = self.inner.entries.read().map_err(|error| { - PluginError::Internal(format!("worker inference registry lock poisoned: {error}")) - })?; - let entry = entries.get(&name).ok_or_else(|| { - PluginError::NotFound(format!("worker inference '{name}' is not registered")) - })?; - if entry.descriptor.contract() != expected_contract { - return Err(PluginError::RegistrationFailed(format!( - "worker inference '{name}' implements contract '{}' but '{}' is required", - entry.descriptor.contract(), - expected_contract - ))); - } - Ok(WorkerInference { - descriptor: entry.descriptor.clone(), - callback: Arc::clone(&entry.callback), - }) - } - - fn deregister(&self, name: &str, registration_id: u64) -> Result { - let mut entries = self.inner.entries.write().map_err(|error| { - PluginError::Internal(format!("worker inference registry lock poisoned: {error}")) - })?; - match entries.get(name) { - Some(entry) if entry.registration_id == registration_id => { - entries.remove(name); - Ok(PluginDeregistrationOutcome::Removed) - } - Some(_) => Ok(PluginDeregistrationOutcome::Replaced), - None => Ok(PluginDeregistrationOutcome::Missing), - } - } -} - -/// Owned registration for one worker inference callback in a host registry. -#[doc(hidden)] -pub struct WorkerInferenceRegistration { - registry: WorkerInferenceRegistry, - name: String, - registration_id: Option, -} - -impl WorkerInferenceRegistration { - /// Returns the registered worker inference name. - pub fn name(&self) -> &str { - &self.name - } - - pub(crate) fn deregister_checked(&mut self) -> Result { - let Some(registration_id) = self.registration_id.take() else { - return Ok(PluginDeregistrationOutcome::Missing); - }; - self.registry.deregister(&self.name, registration_id) - } -} - -impl Drop for WorkerInferenceRegistration { - fn drop(&mut self) { - if let Err(error) = self.deregister_checked() { - log::error!( - target: "nemo_relay.plugin", - event = "worker_inference_cleanup_failed", - worker_inference = self.name.as_str(); - "Worker inference cleanup failed during drop: {error}" - ); - } - } -} - -fn normalized_identifier(value: String, field: &str) -> Result { - let normalized = value.trim(); - if normalized.is_empty() { - return Err(PluginError::RegistrationFailed(format!( - "{field} must not be empty" - ))); - } - Ok(normalized.to_string()) -} diff --git a/crates/core/tests/fixtures/worker_plugin/src/main.rs b/crates/core/tests/fixtures/worker_plugin/src/main.rs index 8ad3b6755..a9dd41a67 100644 --- a/crates/core/tests/fixtures/worker_plugin/src/main.rs +++ b/crates/core/tests/fixtures/worker_plugin/src/main.rs @@ -12,8 +12,6 @@ use serde_json::json; struct FixtureWorkerPlugin; -const DEFAULT_INFERENCE_CONTRACT: &str = "nemo.relay.pii_detection.v1"; - impl WorkerPlugin for FixtureWorkerPlugin { fn plugin_id(&self) -> &str { if std::env::var("FIXTURE_WORKER_PLUGIN_ID").as_deref() == Ok("other_worker") { @@ -59,70 +57,6 @@ impl WorkerPlugin for FixtureWorkerPlugin { ctx.register_subscriber("", |_| {}); return Ok(()); } - let worker_inference_names = config - .get("worker_inference_names") - .and_then(Json::as_array) - .map(|names| { - names - .iter() - .filter_map(Json::as_str) - .map(str::to_string) - .collect::>() - }) - .unwrap_or_else(|| { - vec![ - config - .get("worker_inference_name") - .and_then(Json::as_str) - .unwrap_or("fixture_local_model") - .to_string(), - ] - }); - for inference_name in worker_inference_names { - let callback_inference_name = inference_name.clone(); - let exit_in_local_model = fixture_flag(config, "exit_in_local_model"); - let contract = config - .get("worker_inference_contract") - .and_then(Json::as_str) - .unwrap_or(DEFAULT_INFERENCE_CONTRACT); - ctx.register_worker_inference(&inference_name, contract, move |request| { - let inference_name = callback_inference_name.clone(); - async move { - if exit_in_local_model { - std::process::exit(45); - } - if let Some(delay_ms) = request.get("delay_ms").and_then(Json::as_u64) { - tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; - } - if let Some(texts) = request.get("texts").and_then(Json::as_array) { - let detections = texts - .iter() - .filter_map(|item| { - let text_id = item.get("id")?.as_u64()?; - let text = item.get("text")?.as_str()?; - let start_utf8 = text.find("PRIVATE")?; - Some(json!({ - "text_id": text_id, - "start_utf8": start_utf8, - "end_utf8": start_utf8 + "PRIVATE".len(), - "label": "fixture_private", - "score": 1.0 - })) - }) - .collect::>(); - return Ok(json!({"version": 1, "detections": detections})); - } - Ok(json!({ - "version": 1, - "request": request, - "worker_inference": inference_name - })) - } - }); - } - if fixture_flag(config, "worker_inference_only") { - return Ok(()); - } let runtime = ctx .runtime() diff --git a/crates/core/tests/integration/worker_plugin_tests.rs b/crates/core/tests/integration/worker_plugin_tests.rs index 692b9e943..4387b048a 100644 --- a/crates/core/tests/integration/worker_plugin_tests.rs +++ b/crates/core/tests/integration/worker_plugin_tests.rs @@ -24,19 +24,17 @@ use nemo_relay::codec::traits::LlmCodec; use nemo_relay::error::Result as FlowResult; use nemo_relay::plugin::dynamic::{ DynamicPluginActivationSpec, DynamicPluginKind, PluginHostActivation, WorkerPluginActivation, - WorkerPluginLoadSpec, load_worker_plugins, load_worker_plugins_with_worker_inference, + WorkerPluginLoadSpec, load_worker_plugins, }; use nemo_relay::plugin::{ - PluginComponentSpec, PluginConfig, WorkerInferenceDescriptor, WorkerInferenceRegistry, - clear_plugin_configuration, initialize_plugins_exact, - initialize_plugins_exact_with_worker_inference, list_plugin_kinds, + PluginComponentSpec, PluginConfig, clear_plugin_configuration, initialize_plugins_exact, + list_plugin_kinds, }; use serde_json::{Map, Value as Json, json}; use sha2::{Digest, Sha256}; use tempfile::TempDir; use uuid::Uuid; -const PII_DETECTION_CONTRACT: &str = "nemo.relay.pii_detection.v1"; static WORKER_PLUGIN_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); fn enable_operational_logs() { @@ -52,203 +50,6 @@ fn worker_activation_with_no_specs_is_empty() { activation.clear(); } -#[tokio::test(flavor = "multi_thread")] -async fn worker_inference_is_preinstalled_times_out_and_clears() { - let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; - let fixture = build_fixture_worker(); - let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); - let activation = load_worker_plugins([WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::new(), - }]) - .expect("worker plugin should load"); - let registry = activation.worker_inference_registry(); - - // Worker inference must be available before static consumers initialize. - let inference = registry - .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) - .expect("worker inference should be installed"); - assert_eq!( - inference - .invoke( - json!({"text": "private"}), - std::time::Duration::from_secs(1) - ) - .expect("worker inference should return JSON"), - json!({ - "version": 1, - "request": {"text": "private"}, - "worker_inference": "fixture_local_model" - }) - ); - let timeout = inference - .invoke( - json!({"delay_ms": 100}), - std::time::Duration::from_millis(5), - ) - .expect_err("worker inference should honor the caller deadline") - .to_string(); - assert!(timeout.contains("timed out"), "{timeout}"); - - activation.clear(); - assert!( - registry - .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) - .is_err(), - "inference should be removed when the worker activation clears" - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn worker_clear_fails_an_in_flight_local_model_call_without_hanging() { - let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; - let fixture = build_fixture_worker(); - let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); - let activation = load_worker_plugins([WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::new(), - }]) - .expect("worker plugin should load"); - let registry = activation.worker_inference_registry(); - let inference = registry - .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) - .expect("worker inference should be installed"); - let invocation = std::thread::spawn(move || { - inference.invoke( - json!({"delay_ms": 5_000}), - std::time::Duration::from_secs(10), - ) - }); - - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - activation.clear(); - - let error = invocation - .join() - .expect("inference invocation thread should join") - .expect_err("clearing the worker must fail its in-flight call") - .to_string(); - assert!( - error.contains("invocation failed") || error.contains("cancel"), - "{error}" - ); - assert!( - registry - .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT) - .is_err(), - "inference should remain deregistered after concurrent clear" - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn worker_inference_rolls_back_after_later_plugin_registration_failure() { - let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; - let fixture = build_fixture_worker(); - let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); - let first_inference = "fixture_local_model_first"; - let first_inference_key = format!("fixture_worker/{first_inference}"); - let registry = WorkerInferenceRegistry::default(); - let first = load_worker_plugins_with_worker_inference( - [WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([("worker_inference_name".into(), json!(first_inference))]), - }], - registry.clone(), - ) - .expect("first worker plugin should load"); - - let second_inference = "fixture_local_model_rollback"; - let second_inference_key = format!("fixture_worker/{second_inference}"); - let second = load_worker_plugins_with_worker_inference( - [WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([("worker_inference_name".into(), json!(second_inference))]), - }], - registry.clone(), - ); - assert!( - second.is_err(), - "duplicate plugin kind should fail after the second inference is installed" - ); - assert!( - registry - .resolve(&second_inference_key, PII_DETECTION_CONTRACT) - .is_err(), - "the second inference must be rolled back with its failed activation" - ); - assert!( - registry - .resolve(&first_inference_key, PII_DETECTION_CONTRACT) - .is_ok(), - "rollback must not remove the first activation's inference" - ); - - first.clear(); - assert!( - registry - .resolve(&first_inference_key, PII_DETECTION_CONTRACT) - .is_err() - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn worker_inference_rolls_back_earlier_inference_after_same_worker_conflict() { - let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; - let fixture = build_fixture_worker(); - let (_manifest_dir, manifest_ref) = write_manifest(fixture.binary_path()); - let first_inference_key = "fixture_worker/fixture_local_model_unique"; - let conflicting_inference_key = "fixture_worker/fixture_local_model_conflict"; - let registry = WorkerInferenceRegistry::default(); - let _existing_registration = registry - .register( - WorkerInferenceDescriptor::new(conflicting_inference_key, PII_DETECTION_CONTRACT) - .unwrap(), - Arc::new(|request, _| Ok(json!({"existing": request}))), - ) - .expect("conflicting inference fixture should register"); - - let activation = load_worker_plugins_with_worker_inference( - [WorkerPluginLoadSpec { - plugin_id: "fixture_worker".into(), - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([( - "worker_inference_names".into(), - json!(["fixture_local_model_unique", "fixture_local_model_conflict"]), - )]), - }], - registry.clone(), - ); - - assert!( - activation.is_err(), - "the worker activation should fail on its second inference" - ); - assert!( - registry - .resolve(first_inference_key, PII_DETECTION_CONTRACT) - .is_err(), - "an earlier inference from the failed worker must be rolled back" - ); - let existing = registry - .resolve(conflicting_inference_key, PII_DETECTION_CONTRACT) - .expect("the existing conflicting inference must remain registered"); - assert_eq!( - existing - .invoke(json!({"value": 1}), std::time::Duration::from_secs(1)) - .expect("existing inference should remain callable"), - json!({"existing": {"value": 1}}) - ); -} - #[tokio::test] async fn plugin_host_activation_owns_worker_lifecycle() { let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; @@ -1435,7 +1236,6 @@ async fn load_and_initialize_fixture(config: Map) -> LoadedWorker config: config.clone(), }]) .expect("worker plugin should load"); - let worker_inference = activation.worker_inference_registry(); let mut plugin_config = PluginConfig::default(); plugin_config.components.push(PluginComponentSpec { @@ -1443,7 +1243,7 @@ async fn load_and_initialize_fixture(config: Map) -> LoadedWorker enabled: true, config, }); - initialize_plugins_exact_with_worker_inference(plugin_config, worker_inference) + initialize_plugins_exact(plugin_config) .await .expect("worker plugin should initialize"); diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index f3e515cb5..7754238dc 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -246,7 +246,6 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { surface: RegistrationSurface::Subscriber as i32, priority: 0, break_chain: false, - contract: String::new(), }], error: None, }, @@ -262,7 +261,6 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { surface: 999, priority: 0, break_chain: false, - contract: String::new(), }], error: None, }, @@ -282,7 +280,6 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { surface: RegistrationSurface::Unspecified as i32, priority: 0, break_chain: false, - contract: String::new(), }], error: None, }, @@ -294,44 +291,6 @@ fn registration_plan_and_scope_type_helpers_validate_edges() { .contains("unspecified registration surface") ); - let missing_contract = validate_registration_plan( - "fixture_worker", - &RegisterResponse { - registrations: vec![registration( - RegistrationSurface::WorkerInference, - "detector", - )], - error: None, - }, - ) - .expect_err("worker inference must declare a contract"); - assert!(missing_contract.to_string().contains("without a contract")); - - let contract_on_middleware = validate_registration_plan( - "fixture_worker", - &RegisterResponse { - registrations: vec![Registration { - contract: "test.detector.v1".into(), - ..registration(RegistrationSurface::Subscriber, "subscriber") - }], - error: None, - }, - ) - .expect_err("middleware registrations must not declare inference contracts"); - assert!(contract_on_middleware.to_string().contains("non-inference")); - - validate_registration_plan( - "fixture_worker", - &RegisterResponse { - registrations: vec![Registration { - contract: "test.detector.v1".into(), - ..registration(RegistrationSurface::WorkerInference, "detector") - }], - error: None, - }, - ) - .expect("versioned worker inference contract should be accepted"); - let cases = [ (ProtoScopeType::Agent, crate::api::scope::ScopeType::Agent), ( @@ -2189,7 +2148,6 @@ fn registration(surface: RegistrationSurface, local_name: &str) -> Registration surface: surface as i32, priority: 0, break_chain: false, - contract: String::new(), } } diff --git a/crates/core/tests/unit/plugin_tests.rs b/crates/core/tests/unit/plugin_tests.rs index 9055009cb..b39abb9d2 100644 --- a/crates/core/tests/unit/plugin_tests.rs +++ b/crates/core/tests/unit/plugin_tests.rs @@ -26,7 +26,6 @@ struct RecordingPlugin; struct ReplacementPlugin; struct RestoreFailPlugin; struct RestoreBreakPlugin; -struct WorkerInferenceAwarePlugin; struct PartialFailPlugin; struct VanishingPlugin; struct BlockingPlugin { @@ -56,16 +55,11 @@ static PARTIAL_FAIL_ROLLBACKS: AtomicUsize = AtomicUsize::new(0); static RESTORE_FAIL_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); static RESTORE_BREAK_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); static REPLACEMENT_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0); -static WORKER_INFERENCE_VALUES: OnceLock>> = OnceLock::new(); fn recorded_names() -> &'static Mutex> { RECORDED_NAMES.get_or_init(|| Mutex::new(Vec::new())) } -fn worker_inference_values() -> &'static Mutex> { - WORKER_INFERENCE_VALUES.get_or_init(|| Mutex::new(Vec::new())) -} - fn lock_runtime_owner() -> std::sync::MutexGuard<'static, ()> { crate::shared_runtime::runtime_owner_test_mutex() .lock() @@ -311,40 +305,6 @@ impl Plugin for RestoreBreakPlugin { } } -impl Plugin for WorkerInferenceAwarePlugin { - fn plugin_kind(&self) -> &str { - "worker-inference-aware.plugin" - } - - fn validate(&self, _plugin_config: &Map) -> Vec { - vec![] - } - - fn register<'a>( - &'a self, - _plugin_config: &Map, - ctx: &'a mut PluginRegistrationContext, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - let inference = ctx.worker_inference("shared-inference", "test.echo.v1")?; - let response = inference.invoke(json!({}), std::time::Duration::from_secs(1))?; - let source = response - .get("source") - .and_then(Json::as_str) - .ok_or_else(|| { - PluginError::RegistrationFailed( - "worker-inference-aware.plugin received an invalid response".into(), - ) - })?; - worker_inference_values() - .lock() - .unwrap() - .push(source.to_string()); - Ok(()) - }) - } -} - impl Plugin for PartialFailPlugin { fn plugin_kind(&self) -> &str { "partial.fail.plugin" @@ -517,14 +477,12 @@ fn reset_global() { RESTORE_FAIL_REGISTRATIONS.store(0, Ordering::SeqCst); RESTORE_BREAK_REGISTRATIONS.store(0, Ordering::SeqCst); REPLACEMENT_REGISTRATIONS.store(0, Ordering::SeqCst); - worker_inference_values().lock().unwrap().clear(); let _ = deregister_plugin("test.plugin"); let _ = deregister_plugin("singleton.plugin"); let _ = deregister_plugin("recording.plugin"); let _ = deregister_plugin("replacement.plugin"); let _ = deregister_plugin("restore.fail.plugin"); let _ = deregister_plugin("restore.break.plugin"); - let _ = deregister_plugin("worker-inference-aware.plugin"); let _ = deregister_plugin("partial.fail.plugin"); let _ = deregister_plugin("vanishing.plugin"); let _ = deregister_plugin("blocking.plugin"); @@ -1152,60 +1110,6 @@ fn test_initialize_plugins_restores_previous_configuration_after_failed_replacem reset_global(); } -#[test] -fn test_failed_replacement_restores_previous_worker_inference_registry() { - let _guard = lock_runtime_owner(); - reset_global(); - register_plugin(Arc::new(WorkerInferenceAwarePlugin)).unwrap(); - register_plugin(Arc::new(RestoreFailPlugin)).unwrap(); - - let previous_registry = WorkerInferenceRegistry::default(); - let _previous_inference = previous_registry - .register( - WorkerInferenceDescriptor::new("shared-inference", "test.echo.v1").unwrap(), - Arc::new(|_, _| Ok(json!({"source": "previous"}))), - ) - .unwrap(); - let replacement_registry = WorkerInferenceRegistry::default(); - let _replacement_inference = replacement_registry - .register( - WorkerInferenceDescriptor::new("shared-inference", "test.echo.v1").unwrap(), - Arc::new(|_, _| Ok(json!({"source": "replacement"}))), - ) - .unwrap(); - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - runtime - .block_on(initialize_plugins_exact_with_worker_inference( - PluginConfig { - components: vec![PluginComponentSpec::new("worker-inference-aware.plugin")], - ..PluginConfig::default() - }, - previous_registry, - )) - .unwrap(); - - let error = runtime - .block_on(initialize_plugins_exact_with_worker_inference( - PluginConfig { - components: vec![PluginComponentSpec::new("restore.fail.plugin")], - ..PluginConfig::default() - }, - replacement_registry, - )) - .unwrap_err(); - assert!(error.to_string().contains("refused to initialize")); - assert_eq!( - *worker_inference_values().lock().unwrap(), - vec!["previous", "previous"] - ); - - reset_global(); -} - #[test] fn test_initialize_plugins_restores_previous_configuration_after_replacement_panic() { let _guard = lock_runtime_owner(); @@ -1414,7 +1318,6 @@ fn test_checked_teardown_reports_unremoved_registrations() { )) }), )], - WorkerInferenceRegistry::default(), ) .unwrap(); @@ -1440,7 +1343,6 @@ fn test_legacy_clear_retains_mutation_owner_after_incomplete_teardown() { "stale-callback", Box::new(|| panic!("fixture deregistration panicked")), )], - WorkerInferenceRegistry::default(), ) .unwrap(); diff --git a/crates/core/tests/unit/worker_inference_tests.rs b/crates/core/tests/unit/worker_inference_tests.rs deleted file mode 100644 index 908dc5f32..000000000 --- a/crates/core/tests/unit/worker_inference_tests.rs +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::Arc; -use std::time::Duration; - -use serde_json::json; - -use crate::plugin::{WorkerInferenceDescriptor, WorkerInferenceRegistry}; - -#[test] -fn inference_round_trips_json_and_receives_deadline() { - let registry = WorkerInferenceRegistry::default(); - let _registration = registry - .register( - WorkerInferenceDescriptor::new("test-inference", "test.echo.v1").unwrap(), - Arc::new(|request, timeout| { - assert_eq!(timeout, Duration::from_millis(25)); - Ok(json!({"request": request})) - }), - ) - .unwrap(); - - let inference = registry.resolve("test-inference", "test.echo.v1").unwrap(); - assert_eq!( - inference - .invoke(json!({"text": "hello"}), Duration::from_millis(25)) - .unwrap(), - json!({"request": {"text": "hello"}}) - ); -} - -#[test] -fn registration_owns_inference_lifetime() { - let registry = WorkerInferenceRegistry::default(); - let registration = registry - .register( - WorkerInferenceDescriptor::new("owned-inference", "test.echo.v1").unwrap(), - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - - assert!(registry.resolve("owned-inference", "test.echo.v1").is_ok()); - drop(registration); - assert!(registry.resolve("owned-inference", "test.echo.v1").is_err()); -} - -#[test] -fn duplicate_inference_names_are_rejected() { - let registry = WorkerInferenceRegistry::default(); - let _registration = registry - .register( - WorkerInferenceDescriptor::new("duplicate-inference", "test.echo.v1").unwrap(), - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - let duplicate = registry - .register( - WorkerInferenceDescriptor::new("duplicate-inference", "test.other.v1").unwrap(), - Arc::new(|request, _| Ok(request)), - ) - .err() - .expect("duplicate inference names must fail"); - - assert!(duplicate.to_string().contains("already registered")); -} - -#[test] -fn inference_names_are_normalized_consistently() { - let registry = WorkerInferenceRegistry::default(); - let _registration = registry - .register( - WorkerInferenceDescriptor::new(" normalized-inference ", " test.echo.v1 ").unwrap(), - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - - assert!( - registry - .resolve(" normalized-inference ", " test.echo.v1 ") - .is_ok() - ); -} - -#[test] -fn inference_contract_mismatch_is_rejected_before_invocation() { - let registry = WorkerInferenceRegistry::default(); - let _registration = registry - .register( - WorkerInferenceDescriptor::new("detector", "test.detector.v1").unwrap(), - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - - let error = registry - .resolve("detector", "test.embedding.v1") - .err() - .expect("mismatched contracts must fail"); - assert!(error.to_string().contains("test.detector.v1")); - assert!(error.to_string().contains("test.embedding.v1")); -} - -#[test] -fn registries_isolate_inference_names_between_hosts() { - let first = WorkerInferenceRegistry::default(); - let second = WorkerInferenceRegistry::default(); - let _first_registration = first - .register( - WorkerInferenceDescriptor::new("shared-name", "test.echo.v1").unwrap(), - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); - - assert!(first.resolve("shared-name", "test.echo.v1").is_ok()); - assert!(second.resolve("shared-name", "test.echo.v1").is_err()); -} diff --git a/crates/node/pii_redaction.d.ts b/crates/node/pii_redaction.d.ts index 2df3407ea..5a5f20e76 100644 --- a/crates/node/pii_redaction.d.ts +++ b/crates/node/pii_redaction.d.ts @@ -26,23 +26,10 @@ export interface LocalModelConfig { backend?: string; model_id?: string; detector_profile?: string; - target_paths?: string[]; - target_path_patterns?: string[]; - min_score?: number; - excluded_labels?: string[]; - replacement?: string; allow_network?: boolean; max_latency_ms?: number; } -export interface ProfileConfig { - enabled?: boolean; - mode?: 'builtin' | 'local_model' | string; - priority?: number; - builtin?: BuiltinConfig; - local?: LocalModelConfig; -} - export interface Config { version?: number; mode?: 'builtin' | 'local_model' | string; @@ -53,7 +40,6 @@ export interface Config { mark?: boolean; priority?: number; codec?: 'openai_chat' | 'openai_responses' | 'anthropic_messages' | string; - profiles?: ProfileConfig[]; builtin?: BuiltinConfig; local?: LocalModelConfig; policy?: ConfigPolicy; @@ -71,10 +57,8 @@ export declare const PII_REDACTION_PLUGIN_KIND: 'pii_redaction'; export declare function defaultConfig(): Config; /** Create deterministic built-in redaction backend settings with defaults applied. */ export declare function builtinConfig(config?: BuiltinConfig): BuiltinConfig; -/** Create worker-backed local-model provider settings. */ +/** Create future local-model backend settings with defaults applied. */ export declare function localModelConfig(config?: LocalModelConfig): LocalModelConfig; -/** Create one ordered redaction profile with defaults applied. */ -export declare function profileConfig(config?: ProfileConfig): ProfileConfig; /** Wrap PII redaction config as a top-level plugin component. */ export declare function ComponentSpec( config: Config, diff --git a/crates/node/pii_redaction.js b/crates/node/pii_redaction.js index dea68d566..c5fc6d313 100644 --- a/crates/node/pii_redaction.js +++ b/crates/node/pii_redaction.js @@ -39,7 +39,7 @@ function builtinConfig(config = {}) { } /** - * Create worker-backed local-model redaction settings. + * Create future local-model backend settings with defaults applied. * * @param {object} [config={}] - Partial local-model settings to override. * @returns {object} A normalized local-model backend config object. @@ -50,21 +50,6 @@ function localModelConfig(config = {}) { }; } -/** - * Create one ordered redaction profile with defaults applied. - * - * @param {object} [config={}] - Partial profile settings to override. - * @returns {object} A normalized redaction profile object. - */ -function profileConfig(config = {}) { - return { - enabled: true, - mode: 'builtin', - priority: 100, - ...config, - }; -} - /** * Wrap PII redaction config as a top-level plugin component. * @@ -83,6 +68,5 @@ module.exports = { defaultConfig, builtinConfig, localModelConfig, - profileConfig, ComponentSpec, }; diff --git a/crates/node/tests/pii_redaction_tests.mjs b/crates/node/tests/pii_redaction_tests.mjs index e696e4880..0c0edc3f7 100644 --- a/crates/node/tests/pii_redaction_tests.mjs +++ b/crates/node/tests/pii_redaction_tests.mjs @@ -23,37 +23,6 @@ describe('pii_redaction plugin helpers', () => { }); assert.deepEqual(piiRedaction.builtinConfig(), { action: 'remove' }); assert.deepEqual(piiRedaction.localModelConfig(), {}); - assert.deepEqual( - piiRedaction.localModelConfig({ - backend: 'acme.pii/detector', - model_id: 'pii-model-v1', - detector_profile: 'default', - target_paths: ['/message'], - target_path_patterns: ['/messages/*/content'], - min_score: 0.6, - excluded_labels: ['CITY'], - replacement: '[PRIVATE]', - allow_network: false, - max_latency_ms: 250, - }), - { - backend: 'acme.pii/detector', - model_id: 'pii-model-v1', - detector_profile: 'default', - target_paths: ['/message'], - target_path_patterns: ['/messages/*/content'], - min_score: 0.6, - excluded_labels: ['CITY'], - replacement: '[PRIVATE]', - allow_network: false, - max_latency_ms: 250, - }, - ); - assert.deepEqual(piiRedaction.profileConfig(), { - enabled: true, - mode: 'builtin', - priority: 100, - }); const component = piiRedaction.ComponentSpec({ ...piiRedaction.defaultConfig(), @@ -63,29 +32,6 @@ describe('pii_redaction plugin helpers', () => { assert.equal(component.enabled, true); }); - it('builds profile composition without legacy top-level fields', () => { - const config = { - version: 1, - codec: 'openai_chat', - profiles: [ - piiRedaction.profileConfig({ - builtin: piiRedaction.builtinConfig({ detector: 'email' }), - }), - piiRedaction.profileConfig({ - mode: 'local_model', - priority: 110, - local: piiRedaction.localModelConfig({ - backend: 'acme.pii/detector', - target_paths: ['/message'], - }), - }), - ], - }; - - assert.equal(config.mode, undefined); - assert.deepEqual(plugin.validate({ version: 1, components: [piiRedaction.ComponentSpec(config)] }).diagnostics, []); - }); - it('lists builtin pii_redaction kind and validates bad values', () => { assert.equal(plugin.listKinds().includes(piiRedaction.PII_REDACTION_PLUGIN_KIND), true); const report = plugin.validate({ diff --git a/crates/pii-redaction/Cargo.toml b/crates/pii-redaction/Cargo.toml index fb2163f23..006a45b59 100644 --- a/crates/pii-redaction/Cargo.toml +++ b/crates/pii-redaction/Cargo.toml @@ -27,7 +27,7 @@ sha2 = "0.11" schemars = { version = "0.8", optional = true } [dev-dependencies] -nemo-relay = { workspace = true, features = ["openinference", "otel", "worker-grpc"] } +nemo-relay = { workspace = true, features = ["openinference", "otel"] } futures = "0.3" tokio = { version = "1", features = ["rt", "macros", "sync", "test-util", "rt-multi-thread", "time"] } opentelemetry_sdk = { workspace = true, features = ["trace", "testing"] } diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index 458f187a7..4c8528582 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0 `nemo-relay-pii-redaction` is the first-party NeMo Relay plugin crate for deterministic privacy redaction on tool and LLM observability payloads. It ships the `pii_redaction` plugin contract, a production-ready `builtin` -backend, and a worker-backed `local_model` seam for model-backed detection and +backend, and the future `local_model` seam for model-backed detection and redaction. The plugin is designed for the common case where teams want a supported, @@ -36,8 +36,8 @@ NeMo Relay PII Redaction allows you to: `openai_responses`, and `anthropic_messages`. - Remove conversational trajectory content while preserving event structure, tool-call identity, model attribution, routing, usage, and cost analytics. -- Use an isolated `grpc-v1` worker as the detector behind the `local_model` - config contract without loading model dependencies into the Relay host. +- Use the `local_model` config contract and provider registration surface for + future model-backed implementations. ## Plugin Versus Raw Middleware @@ -188,153 +188,13 @@ high-risk secrets, prefer `redact` over partial `mask` behavior. ## Local Model Seam -`local_model` delegates bounded detector inference to a manifest-backed -`grpc-v1` worker. The PII component remains responsible for choosing -observability fields, decoding provider payloads, batching text, enforcing the -deadline and failure policy, validating detections, and replacing accepted -spans. +`local_model` is included in the plugin contract now, but no runtime +implementation ships in this crate yet. -Configure worker inference by its host-qualified name: - -```toml -[[components]] -kind = "pii_redaction" -enabled = true - -[components.config] -mode = "local_model" -codec = "openai_chat" - -[components.config.local] -backend = "acme.pii_worker/detector" -model_id = "acme-pii-v1" -detector_profile = "default" -min_score = 0.4 -target_path_patterns = [ - "/messages/*/content", - "/messages/*/content/*/text", - "/message", - "/message/*/text", -] -replacement = "[REDACTED]" -allow_network = false -max_latency_ms = 250 -``` - -The backend name is `/`. For example, a worker -with plugin ID `acme.pii_worker` that calls -`register_worker_inference("detector", "nemo.relay.pii_detection.v1", ...)` -is selected as `acme.pii_worker/detector`. Relay verifies the PII contract, -installs worker inference before static components initialize, and removes PII -sanitizers before stopping their worker. - -Use profiles to compose deterministic and contextual detection. The lower -priority runs first: - -```toml -[components.config] -codec = "openai_chat" - -[[components.config.profiles]] -mode = "builtin" -priority = 80 - -[components.config.profiles.builtin] -action = "redact" -detector = "email" - -[[components.config.profiles]] -mode = "local_model" -priority = 90 - -[components.config.profiles.local] -backend = "nemo_relay.pii_rampart/detector" -min_score = 0.4 -max_latency_ms = 5000 -target_path_patterns = [ - "/messages/*/content", - "/messages/*/content/*/text", - "/message", - "/message/*/text", -] -``` - -`target_paths` contains exact JSON pointers. `target_path_patterns` also accepts -`*` as one complete path segment, which is useful for message arrays. When a -codec is configured, paths address the normalized request or response shape; -the content-only patterns above cover the `openai_chat` request and response -shape. Without a codec, they address the original JSON payload. When both lists -are empty, Relay inspects every string leaf and reports a configuration warning. - -Use content-only paths for contextual classifiers. Do not send model names, -tool identifiers, trace IDs, routing fields, or arbitrary provider metadata to -a classifier unless that is an explicit policy choice. - -Relay accepts detections whose confidence is at least `min_score`, which -defaults to `0.4`. `excluded_labels` is an exact, case-sensitive denylist for -detection labels that should remain visible. The host applies both settings -after validating the complete worker response; workers do not own the final -redaction policy. - -Worker failures, timeouts, malformed responses, invalid UTF-8 boundaries, -overlapping spans, and input-limit violations fail closed for the affected -batch. If a configured codec cannot decode or safely re-encode an LLM payload, -Relay omits that request or response payload from the emitted event; it does not -retry normalized selectors against the raw provider shape. -`allow_network = true` is rejected; this lane is for same-machine inference. -This setting is a configuration invariant, not a network sandbox: Relay's -worker launcher does not currently prevent a worker process from opening -sockets. Only install workers whose packaging and runtime behavior satisfy -that policy. The default deadline is 250 ms for the complete selected payload, -including every inference batch. Configuration above 60 seconds is rejected. - -### PII Detection Contract - -The worker receives a versioned JSON request: - -```json -{ - "version": 1, - "model_id": "acme-pii-v1", - "detector_profile": "default", - "texts": [ - {"id": 0, "text": "Contact Alice Rivera"} - ] -} -``` - -It returns detections using UTF-8 byte offsets: - -```json -{ - "version": 1, - "detections": [ - { - "text_id": 0, - "start_utf8": 8, - "end_utf8": 20, - "label": "person", - "score": 0.99 - } - ] -} -``` - -The worker performs inference only. It must not choose Relay surfaces, -traverse arbitrary event fields, or apply replacements itself. Rust and Python -workers have SDK helpers for this registration. Other languages can implement -the same `grpc-v1` protobuf contract directly; Rust, Python, and Node hosts all -consume it through the shared core runtime. - -### Optional Rampart Worker - -The source tree includes an optional -[Rampart worker](./workers/rampart/README.md) that implements this detection -contract with a pinned ONNX token-classification model. It runs in a -Relay-managed Python worker process, keeps ONNX dependencies out of the host, -and complements the built-in deterministic recognizers. The model is prefetched -separately, then resolved and integrity-verified from the local cache at -activation. It is not distributed in the Relay package. +The seam exists so a future local detector/redactor backend can be added +without redesigning the public plugin surface. If `mode = "local_model"` is +configured today, the runtime expects a registered local backend provider and +fails fast if one is not installed. ## Documentation diff --git a/crates/pii-redaction/src/builtin.rs b/crates/pii-redaction/src/builtin.rs index 292da0430..6e6d1dc5c 100644 --- a/crates/pii-redaction/src/builtin.rs +++ b/crates/pii-redaction/src/builtin.rs @@ -534,10 +534,6 @@ pub(super) fn llm_sanitize_request_callback( request.content = backend.sanitize_json_preorder_dfs(request.content); return Some(request); } - if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - request.content = backend.sanitize_json_preorder_dfs(request.content); - return Some(request); - } let resolved = context.resolve_codec(); let fallback = if resolved.is_none() { backend @@ -572,9 +568,6 @@ pub(super) fn llm_sanitize_response_callback( if backend.target_paths.is_empty() { return Some(backend.sanitize_json_preorder_dfs(payload)); } - if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - return Some(backend.sanitize_json_preorder_dfs(payload)); - } if matches!(context.codec(), LlmCodecIdentity::None) && !backend.uses_compatible_legacy_response_codec(&payload) { @@ -703,7 +696,7 @@ fn remove_sanitized_json_pointer_value(value: &mut Json, segments: &[String]) -> } } -pub(super) fn render_json_pointer_path(path_segments: &[String]) -> String { +fn render_json_pointer_path(path_segments: &[String]) -> String { if path_segments.is_empty() { return String::new(); } @@ -715,7 +708,7 @@ pub(super) fn render_json_pointer_path(path_segments: &[String]) -> String { rendered } -pub(super) fn escape_json_pointer_segment(segment: &str) -> String { +fn escape_json_pointer_segment(segment: &str) -> String { segment.replace('~', "~0").replace('/', "~1") } diff --git a/crates/pii-redaction/src/component.rs b/crates/pii-redaction/src/component.rs index 2217c2b21..2f6b9bfd3 100644 --- a/crates/pii-redaction/src/component.rs +++ b/crates/pii-redaction/src/component.rs @@ -24,45 +24,11 @@ use super::builtin::{ #[cfg(test)] pub(crate) use super::builtin::{hex_sha256, mask_text}; use super::detectors::{detector_regex_pattern, supported_detector_summary}; -use super::local::{register_local_backend, validate_local_backend_config}; +use super::local::register_local_backend; +pub use super::local::{clear_local_backend_provider, register_local_backend_provider}; /// The plugin kind reserved for the built-in privacy component. pub const PII_REDACTION_PLUGIN_KIND: &str = "pii_redaction"; -/// Versioned inference contract implemented by PII detection workers. -pub const PII_DETECTION_CONTRACT: &str = "nemo.relay.pii_detection.v1"; -pub(super) const DEFAULT_LOCAL_MODEL_LATENCY_MS: u64 = 250; -pub(super) const DEFAULT_LOCAL_MODEL_MIN_SCORE: f64 = 0.4; -pub(super) const MAX_LOCAL_MODEL_LATENCY_MS: u64 = 60_000; -pub(super) const MAX_LOCAL_MODEL_TARGET_PATHS: usize = 256; -pub(super) const MAX_LOCAL_MODEL_TARGET_PATH_BYTES: usize = 1024; -pub(super) const MAX_LOCAL_MODEL_REPLACEMENT_BYTES: usize = 1024; -pub(super) const MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES: usize = 1024; -pub(super) const MAX_LOCAL_MODEL_EXCLUDED_LABELS: usize = 128; -pub(super) const MAX_LOCAL_MODEL_LABEL_BYTES: usize = 128; -const BUILTIN_BACKEND_CONFIG_FIELDS: &[&str] = &[ - "preset", - "action", - "target_paths", - "pattern", - "detector", - "replacement", - "mask_char", - "unmasked_prefix", - "unmasked_suffix", - "custom_mark_payload_policy", -]; -const LOCAL_BACKEND_CONFIG_FIELDS: &[&str] = &[ - "backend", - "model_id", - "detector_profile", - "target_paths", - "target_path_patterns", - "min_score", - "excluded_labels", - "replacement", - "allow_network", - "max_latency_ms", -]; /// Top-level PII redaction component wrapper. #[derive(Debug, Clone)] @@ -271,38 +237,23 @@ impl Default for BuiltinBackendConfig { } } -/// Local-backend settings for same-machine worker inference. +/// Local-backend settings for a future in-process local-model runtime. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct LocalBackendConfig { - /// Registered worker inference identifier. + /// Optional local-model backend identifier. #[serde(default, skip_serializing_if = "Option::is_none")] pub backend: Option, - /// Optional model identifier passed to the worker. + /// Optional model identifier reserved for future local-model runtimes. #[serde(default, skip_serializing_if = "Option::is_none")] pub model_id: Option, - /// Optional detector profile passed to the worker. + /// Optional detector profile reserved for future local-model runtimes. #[serde(default, skip_serializing_if = "Option::is_none")] pub detector_profile: Option, - /// Exact JSON-pointer paths to inspect. Empty means every string leaf. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_paths: Vec, - /// JSON-pointer patterns to inspect. A `*` segment matches one path segment. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_path_patterns: Vec, - /// Minimum detection confidence accepted for redaction. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub min_score: Option, - /// Detection labels that should not be redacted. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub excluded_labels: Vec, - /// Replacement applied to every accepted detection. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub replacement: Option, - /// Whether the worker may use network calls. + /// Whether a future local-model backend may use network calls. #[serde(default, skip_serializing_if = "Option::is_none")] pub allow_network: Option, - /// Total worker inference deadline for one selected payload in milliseconds. + /// Target latency budget hint for a future local-model backend. #[serde(default, skip_serializing_if = "Option::is_none")] pub max_latency_ms: Option, } @@ -447,11 +398,6 @@ nemo_relay::editor_config! { backend => { label: "backend", kind: String, optional: true }, model_id => { label: "model_id", kind: String, optional: true }, detector_profile => { label: "detector_profile", kind: String, optional: true }, - target_paths => { label: "target_paths", kind: List, list: &nemo_relay::config_editor::STRING_LIST_ITEM }, - target_path_patterns => { label: "target_path_patterns", kind: List, list: &nemo_relay::config_editor::STRING_LIST_ITEM }, - min_score => { label: "min_score", kind: Float, optional: true }, - excluded_labels => { label: "excluded_labels", kind: List, list: &nemo_relay::config_editor::STRING_LIST_ITEM }, - replacement => { label: "replacement", kind: String, optional: true }, allow_network => { label: "allow_network", kind: Boolean, optional: true }, max_latency_ms => { label: "max_latency_ms", kind: Integer, optional: true }, } @@ -704,14 +650,31 @@ fn validate_pii_redaction_plugin_config_with_policy( &config.policy, plugin_config, "builtin", - BUILTIN_BACKEND_CONFIG_FIELDS, + &[ + "preset", + "action", + "target_paths", + "pattern", + "detector", + "replacement", + "mask_char", + "unmasked_prefix", + "unmasked_suffix", + "custom_mark_payload_policy", + ], ); validate_section_fields( &mut diagnostics, &config.policy, plugin_config, "local", - LOCAL_BACKEND_CONFIG_FIELDS, + &[ + "backend", + "model_id", + "detector_profile", + "allow_network", + "max_latency_ms", + ], ); validate_version(&mut diagnostics, &config.policy, config.version); validate_mode(&mut diagnostics, &config.policy, &config); @@ -720,7 +683,6 @@ fn validate_pii_redaction_plugin_config_with_policy( validate_builtin_mode_requirements(&mut diagnostics, &config.policy, plugin_config, &config); validate_builtin_action_requirements(&mut diagnostics, &config.policy, plugin_config, &config); validate_local_mode_requirements(&mut diagnostics, &config.policy, plugin_config, &config); - validate_local_backend_requirements(&mut diagnostics, &config.policy, &config); diagnostics } @@ -790,14 +752,31 @@ fn validate_profile_configuration( &config.policy, raw_profile, "builtin", - BUILTIN_BACKEND_CONFIG_FIELDS, + &[ + "preset", + "action", + "target_paths", + "pattern", + "detector", + "replacement", + "mask_char", + "unmasked_prefix", + "unmasked_suffix", + "custom_mark_payload_policy", + ], ); validate_section_fields( &mut profile_diagnostics, &config.policy, raw_profile, "local", - LOCAL_BACKEND_CONFIG_FIELDS, + &[ + "backend", + "model_id", + "detector_profile", + "allow_network", + "max_latency_ms", + ], ); validate_mode(&mut profile_diagnostics, &config.policy, &profile_config); validate_builtin_mode_requirements( @@ -818,11 +797,16 @@ fn validate_profile_configuration( raw_profile, &profile_config, ); - validate_local_backend_requirements( - &mut profile_diagnostics, - &config.policy, - &profile_config, - ); + if profile.mode == "local_model" && !raw_profile.contains_key("local") { + push_policy_diag( + &mut profile_diagnostics, + config.policy.unsupported_value, + "pii_redaction.unsupported_value", + Some(PII_REDACTION_PLUGIN_KIND.to_string()), + Some("local".to_string()), + "`local` settings are required for a local-model profile".to_string(), + ); + } prefix_profile_diagnostics(&mut profile_diagnostics, index); diagnostics.extend(profile_diagnostics); } @@ -883,16 +867,6 @@ fn validate_local_mode_requirements( config: &PiiRedactionConfig, ) { if config.mode == "local_model" { - if !plugin_config.contains_key("local") { - push_policy_diag( - diagnostics, - policy.unsupported_value, - "pii_redaction.unsupported_value", - Some(PII_REDACTION_PLUGIN_KIND.to_string()), - Some("local".to_string()), - "`local` settings are required when mode = 'local_model'".to_string(), - ); - } return; } if !plugin_config.contains_key("local") { @@ -909,58 +883,6 @@ fn validate_local_mode_requirements( ); } -fn validate_local_backend_requirements( - diagnostics: &mut Vec, - policy: &ConfigPolicy, - config: &PiiRedactionConfig, -) { - if config.mode != "local_model" { - return; - } - let Some(local) = config.local.as_ref() else { - return; - }; - for violation in validate_local_backend_config(local) { - push_policy_diag( - diagnostics, - policy.unsupported_value, - "pii_redaction.unsupported_value", - Some(PII_REDACTION_PLUGIN_KIND.to_string()), - Some(violation.field.to_string()), - violation.message, - ); - } - if local.target_paths.is_empty() && local.target_path_patterns.is_empty() { - diagnostics.push(ConfigDiagnostic { - level: DiagnosticLevel::Warning, - code: "pii_redaction.local_model_all_paths".to_string(), - component: Some(PII_REDACTION_PLUGIN_KIND.to_string()), - field: Some("local.target_paths".to_string()), - message: "local-model PII redaction has no target paths and will inspect every string leaf; configure explicit content paths to avoid classifying identifiers and metadata".to_string(), - }); - } -} - -pub(super) fn is_valid_json_pointer(path: &str) -> bool { - if path.is_empty() { - return true; - } - if !path.starts_with('/') { - return false; - } - let mut bytes = path.as_bytes().iter().copied(); - while let Some(byte) = bytes.next() { - if byte == b'~' && !matches!(bytes.next(), Some(b'0' | b'1')) { - return false; - } - } - true -} - -pub(super) fn is_valid_json_pointer_pattern(path: &str) -> bool { - is_valid_json_pointer(path) -} - fn validate_builtin_mode_requirements( diagnostics: &mut Vec, policy: &ConfigPolicy, diff --git a/crates/pii-redaction/src/local.rs b/crates/pii-redaction/src/local.rs index 0ff9732a9..b1198c43b 100644 --- a/crates/pii-redaction/src/local.rs +++ b/crates/pii-redaction/src/local.rs @@ -1,594 +1,44 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::sync::{Arc, LazyLock, Mutex, MutexGuard}; -use nemo_relay::api::event::Event; -use nemo_relay::api::llm::LlmRequest; -use nemo_relay::api::runtime::{ - BuiltinLlmCodec, EventSanitizeFn, LlmCodecIdentity, LlmSanitizeRequestFn, - LlmSanitizeResponseFn, ToolSanitizeFn, -}; -use nemo_relay::codec::resolve::{ - ProviderSurface, detect_response_surface, request_codec as build_request_codec, - response_codec as build_response_codec, -}; -use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_relay::plugin::{ - PluginError, PluginRegistrationContext, Result as PluginResult, WorkerInference, -}; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value as Json}; - -use super::component::{ - DEFAULT_LOCAL_MODEL_LATENCY_MS, DEFAULT_LOCAL_MODEL_MIN_SCORE, LocalBackendConfig, - MAX_LOCAL_MODEL_EXCLUDED_LABELS, MAX_LOCAL_MODEL_LABEL_BYTES, MAX_LOCAL_MODEL_LATENCY_MS, - MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES, MAX_LOCAL_MODEL_REPLACEMENT_BYTES, - MAX_LOCAL_MODEL_TARGET_PATH_BYTES, MAX_LOCAL_MODEL_TARGET_PATHS, PII_DETECTION_CONTRACT, - PiiRedactionConfig, is_valid_json_pointer, is_valid_json_pointer_pattern, - profile_registration_prefix, + PluginError, PluginRegistrationContext, Result as PluginResult, rollback_registrations, }; -use super::overlay::BuiltinCodecName; - -const LOCAL_MODEL_CONTRACT_VERSION: u32 = 1; -const MAX_BATCH_ITEMS: usize = 64; -const MAX_BATCH_BYTES: usize = 64 * 1024; -const MAX_TEXT_BYTES: usize = 16 * 1024; -const MAX_TEXTS_PER_PAYLOAD: usize = 256; -const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; -const MAX_DETECTIONS_PER_TEXT: usize = 128; - -#[derive(Clone)] -struct CompiledLocalBackend { - inference_name: Arc, - inference: WorkerInference, - model_id: Option, - detector_profile: Option, - target_paths: Arc>>, - target_path_patterns: Arc>, - min_score: f64, - excluded_labels: Arc>, - replacement: Arc, - timeout: Duration, - legacy_surface: Option, -} - -#[derive(Clone)] -struct JsonPointerPattern { - segments: Vec, -} - -impl JsonPointerPattern { - fn compile(pattern: String) -> Self { - Self { - segments: compile_json_pointer(pattern), - } - } - - fn matches(&self, path: &[String]) -> bool { - self.segments.len() == path.len() - && self - .segments - .iter() - .zip(path) - .all(|(pattern, segment)| pattern == "*" || pattern == segment) - } -} -#[derive(Serialize)] -struct LocalModelRequest<'a> { - version: u32, - #[serde(skip_serializing_if = "Option::is_none")] - model_id: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - detector_profile: Option<&'a str>, - texts: Vec>, -} +use super::component::PiiRedactionConfig; +use super::component::profile_registration_prefix; -#[derive(Serialize)] -struct LocalModelText<'a> { - id: u32, - text: &'a str, -} +#[doc(hidden)] +pub type LocalBackendProvider = Arc< + dyn Fn(PiiRedactionConfig, &mut PluginRegistrationContext) -> PluginResult<()> + Send + Sync, +>; -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct LocalModelResponse { - version: u32, - detections: Vec, -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct LocalModelDetection { - text_id: u32, - start_utf8: usize, - end_utf8: usize, - label: String, - score: f64, -} - -struct SelectedText { - text: String, - eligible: bool, -} +static LOCAL_BACKEND_PROVIDER: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); -enum EventField { - Data, - CategoryProfile, - Metadata, +fn local_backend_provider_guard() -> PluginResult>> +{ + LOCAL_BACKEND_PROVIDER.lock().map_err(|e| { + PluginError::Internal(format!( + "PII redaction local backend provider lock poisoned: {e}" + )) + }) } -impl CompiledLocalBackend { - fn new( - config: LocalBackendConfig, - codec_name: Option, - ctx: &PluginRegistrationContext, - ) -> PluginResult { - if let Some(violation) = validate_local_backend_config(&config).into_iter().next() { - return Err(PluginError::InvalidConfig(violation.message)); - } - let inference_name = config - .backend - .as_deref() - .map(str::trim) - .expect("validated local backend has a worker inference name") - .to_string(); - let min_score = config.min_score.unwrap_or(DEFAULT_LOCAL_MODEL_MIN_SCORE); - let replacement = config - .replacement - .unwrap_or_else(|| "[REDACTED]".to_string()); - let max_latency_ms = config - .max_latency_ms - .unwrap_or(DEFAULT_LOCAL_MODEL_LATENCY_MS); - let surface = match codec_name.as_deref() { - Some(name) => Some(ProviderSurface::from_codec_name(name).ok_or_else(|| { - PluginError::InvalidConfig(format!("unsupported codec '{name}'")) - })?), - None => None, - }; - let inference = ctx - .worker_inference(&inference_name, PII_DETECTION_CONTRACT) - .map_err(|error| { - PluginError::RegistrationFailed(format!( - "PII detection worker '{inference_name}' is unavailable: {error}" - )) - })?; - Ok(Self { - inference_name: Arc::new(inference_name), - inference, - model_id: config.model_id.map(|value| value.trim().to_string()), - detector_profile: config - .detector_profile - .map(|value| value.trim().to_string()), - target_paths: Arc::new( - config - .target_paths - .into_iter() - .map(compile_json_pointer) - .collect(), - ), - target_path_patterns: Arc::new( - config - .target_path_patterns - .into_iter() - .map(JsonPointerPattern::compile) - .collect(), - ), - min_score, - excluded_labels: Arc::new( - config - .excluded_labels - .into_iter() - .map(|label| label.trim().to_string()) - .collect(), - ), - replacement: Arc::new(replacement), - timeout: Duration::from_millis(max_latency_ms), - legacy_surface: surface, - }) - } - - fn sanitize_json(&self, value: Json) -> Json { - self.sanitize_json_values(vec![value]) - .pop() - .expect("single-value sanitization returns one value") - } - - fn sanitize_json_values(&self, values: Vec) -> Vec { - self.sanitize_json_roots( - values - .into_iter() - .map(|value| (Vec::new(), value)) - .collect(), - ) - } - - fn sanitize_json_roots(&self, mut roots: Vec<(Vec, Json)>) -> Vec { - let mut texts = Vec::new(); - let mut total_bytes = 0; - let mut within_budget = true; - for (path, value) in &mut roots { - self.collect_strings( - value, - path, - &mut texts, - &mut total_bytes, - &mut within_budget, - ); - } - let sanitized = self.sanitize_texts(texts); - let mut index = 0; - for (path, value) in &mut roots { - self.replace_strings(value, path, &sanitized, &mut index); - } - roots.into_iter().map(|(_, value)| value).collect() - } - - fn collect_strings( - &self, - value: &Json, - path: &mut Vec, - texts: &mut Vec, - total_bytes: &mut usize, - within_budget: &mut bool, - ) { - match value { - Json::String(text) if self.matches_path(path) && *within_budget => { - if texts.len() >= MAX_TEXTS_PER_PAYLOAD { - *within_budget = false; - return; - } - if text.len() > MAX_TEXT_BYTES { - texts.push(SelectedText { - text: self.replacement.as_str().to_string(), - eligible: false, - }); - return; - } - let Some(next_total) = total_bytes.checked_add(text.len()) else { - *within_budget = false; - return; - }; - if next_total > MAX_PAYLOAD_TEXT_BYTES { - *within_budget = false; - return; - } - *total_bytes = next_total; - texts.push(SelectedText { - text: text.clone(), - eligible: true, - }); - } - Json::Array(items) => { - for (index, item) in items.iter().enumerate() { - path.push(index.to_string()); - self.collect_strings(item, path, texts, total_bytes, within_budget); - path.pop(); - } - } - Json::Object(fields) => { - for (key, value) in fields { - path.push(super::builtin::escape_json_pointer_segment(key)); - self.collect_strings(value, path, texts, total_bytes, within_budget); - path.pop(); - } - } - _ => {} - } - } - - fn replace_strings( - &self, - value: &mut Json, - path: &mut Vec, - sanitized: &[String], - index: &mut usize, - ) { - match value { - Json::String(text) if self.matches_path(path) => { - if let Some(replacement) = sanitized.get(*index) { - *text = replacement.clone(); - } else { - *text = self.replacement.as_str().to_string(); - } - *index += 1; - } - Json::Array(items) => { - for (item_index, item) in items.iter_mut().enumerate() { - path.push(item_index.to_string()); - self.replace_strings(item, path, sanitized, index); - path.pop(); - } - } - Json::Object(fields) => { - for (key, value) in fields { - path.push(super::builtin::escape_json_pointer_segment(key)); - self.replace_strings(value, path, sanitized, index); - path.pop(); - } - } - _ => {} - } - } - - fn matches_path(&self, path: &[String]) -> bool { - (self.target_paths.is_empty() && self.target_path_patterns.is_empty()) - || self.target_paths.contains(path) - || self - .target_path_patterns - .iter() - .any(|pattern| pattern.matches(path)) - } - - fn sanitize_texts(&self, mut texts: Vec) -> Vec { - let eligible = texts - .iter() - .enumerate() - .filter_map(|(index, text)| text.eligible.then_some(index)) - .collect::>(); - - let mut cursor = 0; - let deadline = Instant::now() + self.timeout; - while cursor < eligible.len() { - let start = cursor; - let mut batch_bytes = 0; - while cursor < eligible.len() && cursor - start < MAX_BATCH_ITEMS { - let next_bytes = texts[eligible[cursor]].text.len(); - if cursor > start && batch_bytes + next_bytes > MAX_BATCH_BYTES { - break; - } - batch_bytes += next_bytes; - cursor += 1; - } - let batch = &eligible[start..cursor]; - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - for index in &eligible[start..] { - texts[*index].text = self.replacement.as_str().to_string(); - } - break; - } - match self.sanitize_batch(&texts, batch, remaining) { - Ok(replacements) => { - for (index, replacement) in replacements { - texts[index].text = replacement; - } - } - Err(_) => { - log::warn!( - target: "nemo_relay.plugin", - event = "local_model_inference_failed", - plugin_kind = "pii_redaction", - worker_inference = self.inference_name.as_str(), - batch_size = batch.len(), - reason = "inference_or_response"; - "PII local-model inference failed closed" - ); - for index in batch { - texts[*index].text = self.replacement.as_str().to_string(); - } - } - } - } - texts.into_iter().map(|selected| selected.text).collect() - } - - fn sanitize_batch( - &self, - texts: &[SelectedText], - batch: &[usize], - timeout: Duration, - ) -> PluginResult> { - let request = LocalModelRequest { - version: LOCAL_MODEL_CONTRACT_VERSION, - model_id: self.model_id.as_deref(), - detector_profile: self.detector_profile.as_deref(), - texts: batch - .iter() - .map(|index| LocalModelText { - id: u32::try_from(*index).expect("bounded text index fits u32"), - text: &texts[*index].text, - }) - .collect(), - }; - let request = serde_json::to_value(request)?; - let response = self.inference.invoke(request, timeout)?; - let response: LocalModelResponse = serde_json::from_value(response).map_err(|error| { - PluginError::RegistrationFailed(format!( - "PII detection worker returned an invalid response: {error}" - )) - })?; - self.apply_response(texts, batch, response) - } - - fn apply_response( - &self, - texts: &[SelectedText], - batch: &[usize], - response: LocalModelResponse, - ) -> PluginResult> { - if response.version != LOCAL_MODEL_CONTRACT_VERSION { - return Err(PluginError::RegistrationFailed(format!( - "unsupported local-model response version {}", - response.version - ))); - } - if response.detections.len() > batch.len() * MAX_DETECTIONS_PER_TEXT { - return Err(PluginError::RegistrationFailed( - "local-model response exceeded the detection limit".into(), - )); - } - let allowed_ids = batch - .iter() - .map(|index| u32::try_from(*index).expect("bounded text index fits u32")) - .collect::>(); - let mut detections = HashMap::>::new(); - for detection in response.detections { - if !allowed_ids.contains(&detection.text_id) { - return Err(PluginError::RegistrationFailed(format!( - "local-model response referenced unknown text id {}", - detection.text_id - ))); - } - if detection.label.trim().is_empty() - || detection.label.len() > MAX_LOCAL_MODEL_LABEL_BYTES - { - return Err(PluginError::RegistrationFailed( - "local-model response contained an invalid detection label".into(), - )); - } - if !detection.score.is_finite() || !(0.0..=1.0).contains(&detection.score) { - return Err(PluginError::RegistrationFailed( - "local-model response contained an invalid detection score".into(), - )); - } - let text_detections = detections.entry(detection.text_id).or_default(); - if text_detections.len() >= MAX_DETECTIONS_PER_TEXT { - return Err(PluginError::RegistrationFailed(format!( - "local-model response exceeded the per-text detection limit of {MAX_DETECTIONS_PER_TEXT}" - ))); - } - text_detections.push(detection); - } - let mut replacements = Vec::new(); - for index in batch { - let id = u32::try_from(*index).expect("bounded text index fits u32"); - let Some(mut spans) = detections.remove(&id) else { - continue; - }; - spans.sort_by_key(|span| (span.start_utf8, span.end_utf8)); - let text = &texts[*index].text; - let mut previous_end = 0; - for span in &spans { - if span.start_utf8 >= span.end_utf8 - || span.end_utf8 > text.len() - || !text.is_char_boundary(span.start_utf8) - || !text.is_char_boundary(span.end_utf8) - || span.start_utf8 < previous_end - { - return Err(PluginError::RegistrationFailed( - "local-model response contained invalid or overlapping UTF-8 spans".into(), - )); - } - previous_end = span.end_utf8; - } - spans.retain(|detection| { - detection.score >= self.min_score - && !self.excluded_labels.contains(&detection.label) - }); - if spans.is_empty() { - continue; - } - let mut redacted = text.clone(); - for span in spans.iter().rev() { - redacted.replace_range(span.start_utf8..span.end_utf8, self.replacement.as_str()); - } - replacements.push((*index, redacted)); - } - Ok(replacements) - } - - fn sanitize_request_with_codec( - &self, - codec: &dyn LlmCodec, - request: &LlmRequest, - ) -> Option { - let annotated = codec.decode(request).ok()?; - let annotated = serde_json::to_value(annotated).ok()?; - let (headers, annotated) = - self.sanitize_request_parts(request.headers.clone(), annotated)?; - let annotated = serde_json::from_value(annotated).ok()?; - let mut encoded = codec.encode(&annotated, request).ok()?; - encoded.headers = headers; - Some(encoded) - } - - fn sanitize_raw_request(&self, mut request: LlmRequest) -> Option { - let headers = std::mem::take(&mut request.headers); - let content = std::mem::take(&mut request.content); - let (headers, content) = self.sanitize_request_parts(headers, content)?; - request.headers = headers; - request.content = content; - Some(request) - } - - fn sanitize_request_parts( - &self, - headers: Map, - content: Json, - ) -> Option<(Map, Json)> { - let mut values = self.sanitize_json_roots(vec![ - (vec!["headers".to_string()], Json::Object(headers)), - (Vec::new(), content), - ]); - let content = values.pop()?; - let Json::Object(headers) = values.pop()? else { - return None; - }; - Some((headers, content)) - } - - fn sanitize_response_with_codec( - &self, - codec: &dyn LlmResponseCodec, - surface: ProviderSurface, - payload: Json, - ) -> Option { - let codec_name = BuiltinCodecName::from_provider_surface(surface); - let annotated = codec.decode_response(&payload).ok()?; - let sanitized = sanitize_serializable(self, annotated).ok()?; - Some(codec_name.overlay_response_payload(payload, &sanitized)) - } - - fn selected_surface(&self, codec: &LlmCodecIdentity) -> Option { - match codec { - LlmCodecIdentity::None => self.legacy_surface, - LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat) => { - Some(ProviderSurface::OpenAIChat) - } - LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiResponses) => { - Some(ProviderSurface::OpenAIResponses) - } - LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::AnthropicMessages) => { - Some(ProviderSurface::AnthropicMessages) - } - LlmCodecIdentity::Runtime(_) | LlmCodecIdentity::Opaque => None, - } - } - - fn uses_compatible_legacy_response_codec(&self, payload: &Json) -> bool { - self.legacy_surface - .is_some_and(|surface| detect_response_surface(payload) == Some(surface)) - } - - fn log_codec_failure(&self, direction: &'static str, codec: &LlmCodecIdentity, reason: &str) { - let codec_kind = match codec { - LlmCodecIdentity::None => "none", - LlmCodecIdentity::BuiltIn(_) => "builtin", - LlmCodecIdentity::Runtime(_) => "runtime", - LlmCodecIdentity::Opaque => "opaque", - }; - log::warn!( - target: "nemo_relay.plugin", - event = "local_model_codec_failed", - plugin_kind = "pii_redaction", - worker_inference = self.inference_name.as_str(), - direction, - codec_kind, - reason; - "PII local-model payload omitted after codec failure" - ); - } +#[doc(hidden)] +pub fn register_local_backend_provider(provider: LocalBackendProvider) -> PluginResult<()> { + let mut guard = local_backend_provider_guard()?; + *guard = Some(provider); + Ok(()) } -fn compile_json_pointer(pointer: String) -> Vec { - pointer.strip_prefix('/').map_or_else(Vec::new, |path| { - path.split('/').map(str::to_string).collect() - }) +#[doc(hidden)] +pub fn clear_local_backend_provider() -> PluginResult<()> { + let mut guard = local_backend_provider_guard()?; + *guard = None; + Ok(()) } pub(super) fn register_local_backend( @@ -596,395 +46,70 @@ pub(super) fn register_local_backend( ctx: &mut PluginRegistrationContext, profile_name: Option<&str>, ) -> PluginResult<()> { - let local = config.local.clone().ok_or_else(|| { - PluginError::InvalidConfig( - "local settings are required when mode = 'local_model'".to_string(), - ) - })?; - let backend = CompiledLocalBackend::new(local, config.codec.clone(), ctx)?; - - if config.mark { - ctx.register_mark_sanitize_guardrail( - ®istration_name(profile_name, "mark"), - config.priority, - event_sanitize_callback(backend.clone(), None), - )?; - } - if config.tool_input { - ctx.register_tool_sanitize_request_guardrail( - ®istration_name(profile_name, "tool_input"), - config.priority, - tool_sanitize_callback(backend.clone()), - )?; - } - if config.tool_output { - ctx.register_tool_sanitize_response_guardrail( - ®istration_name(profile_name, "tool_output"), - config.priority, - tool_sanitize_callback(backend.clone()), - )?; - } - if config.input { - ctx.register_llm_sanitize_request_guardrail( - ®istration_name(profile_name, "input"), - config.priority, - llm_sanitize_request_callback(backend.clone()), - )?; - } - if config.input || config.tool_input { - ctx.register_scope_sanitize_start_guardrail( - ®istration_name( - profile_name, - if profile_name.is_some() { - "scope_start" - } else { - "input" - }, - ), - config.priority, - event_sanitize_callback(backend.clone(), Some((config.input, config.tool_input))), - )?; - } - if config.output { - ctx.register_llm_sanitize_response_guardrail( - ®istration_name(profile_name, "output"), - config.priority, - llm_sanitize_response_callback(backend.clone()), - )?; - } - if config.output || config.tool_output { - ctx.register_scope_sanitize_end_guardrail( - ®istration_name( - profile_name, - if profile_name.is_some() { - "scope_end" - } else { - "output" - }, - ), - config.priority, - event_sanitize_callback(backend, Some((config.output, config.tool_output))), - )?; - } - Ok(()) -} - -fn tool_sanitize_callback(backend: CompiledLocalBackend) -> ToolSanitizeFn { - Arc::new(move |_name, payload| backend.sanitize_json(payload)) -} - -fn event_sanitize_callback( - backend: CompiledLocalBackend, - scope_categories: Option<(bool, bool)>, -) -> EventSanitizeFn { - Arc::new(move |event, mut fields| { - if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { - matches!(event, Event::Scope(_)) - && event - .category() - .is_some_and(|category| match category.as_str() { - "llm" => !sanitize_llm, - "tool" => !sanitize_tool, - _ => false, - }) - }) { - return fields; - } - let specialized_scope = matches!(event, Event::Scope(_)) - && event - .category() - .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); - - let mut selected = Vec::with_capacity(3); - if !specialized_scope && let Some(data) = fields.data.take() { - selected.push((EventField::Data, data)); - } - if !specialized_scope - && let Some(profile) = fields.category_profile.take() - && let Ok(profile) = serde_json::to_value(profile) - { - selected.push((EventField::CategoryProfile, profile)); - } - if let Some(metadata) = fields.metadata.take() { - selected.push((EventField::Metadata, metadata)); - } - - let values = selected - .iter_mut() - .map(|(_, value)| std::mem::take(value)) - .collect(); - for ((field, _), value) in selected - .into_iter() - .zip(backend.sanitize_json_values(values)) - { - match field { - EventField::Data => fields.data = Some(value), - EventField::CategoryProfile => { - fields.category_profile = serde_json::from_value(value).ok(); - } - EventField::Metadata => fields.metadata = Some(value), - } - } - fields - }) -} + let provider = local_backend_provider_guard()?.clone(); -fn llm_sanitize_request_callback(backend: CompiledLocalBackend) -> LlmSanitizeRequestFn { - Arc::new(move |mut request, context| { - if backend.target_paths.is_empty() && backend.target_path_patterns.is_empty() { - request.content = backend.sanitize_json(request.content); - return Some(request); - } - if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - return backend.sanitize_raw_request(request); - } - let resolved = context.resolve_codec(); - let fallback = if resolved.is_none() { - backend - .selected_surface(context.codec()) - .map(build_request_codec) - } else { - None - }; - let sanitized = resolved - .as_deref() - .or(fallback.as_deref()) - .and_then(|codec| backend.sanitize_request_with_codec(codec, &request)); - if sanitized.is_none() { - backend.log_codec_failure( - "request", - context.codec(), - "codec decode, sanitize, or encode failure", - ); - } - sanitized - }) -} - -fn llm_sanitize_response_callback(backend: CompiledLocalBackend) -> LlmSanitizeResponseFn { - Arc::new(move |payload, context| { - if backend.target_paths.is_empty() && backend.target_path_patterns.is_empty() { - return Some(backend.sanitize_json(payload)); - } - if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - return Some(backend.sanitize_json(payload)); - } - if matches!(context.codec(), LlmCodecIdentity::None) - && !backend.uses_compatible_legacy_response_codec(&payload) - { - backend.log_codec_failure("response", context.codec(), "no compatible legacy codec"); - return None; - } - let surface = backend.selected_surface(context.codec()); - let resolved = context.resolve_codec(); - let fallback = if resolved.is_none() { - surface.map(build_response_codec) - } else { - None - }; - let sanitized = surface - .zip(resolved.as_deref().or(fallback.as_deref())) - .and_then(|(surface, codec)| { - backend.sanitize_response_with_codec(codec, surface, payload) - }); - if sanitized.is_none() { - backend.log_codec_failure( - "response", - context.codec(), - "codec decode, sanitize, or encode failure", - ); - } - sanitized - }) -} - -fn sanitize_serializable(backend: &CompiledLocalBackend, value: T) -> PluginResult -where - T: Serialize + DeserializeOwned, -{ - let value = serde_json::to_value(value)?; - serde_json::from_value(backend.sanitize_json(value)).map_err(PluginError::from) -} - -fn registration_name(profile_name: Option<&str>, callback_name: &str) -> String { - profile_name.map_or_else( - || callback_name.to_string(), - |profile_name| { - format!( - "{}/{callback_name}", - profile_registration_prefix(profile_name) - ) - }, - ) -} - -pub(super) struct LocalConfigViolation { - pub(super) field: &'static str, - pub(super) message: String, -} - -pub(super) fn validate_local_backend_config( - config: &LocalBackendConfig, -) -> Vec { - let mut violations = Vec::new(); - let mut push = |field, message| violations.push(LocalConfigViolation { field, message }); - - match config.backend.as_deref().map(str::trim) { - None | Some("") => push( - "local.backend", - "local.backend is required when mode = 'local_model'".into(), - ), - Some(backend) if backend.len() > MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES => push( - "local.backend", - format!( - "local.backend must not exceed {MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES} UTF-8 bytes" - ), - ), - Some(_) => {} - } - if config.allow_network == Some(true) { - push( - "local.allow_network", - "worker-backed local models must not use network inference".into(), + let Some(provider) = provider else { + log::warn!( + target: "nemo_relay.plugin", + event = "plugin_resource_access_failed", + plugin_kind = "pii_redaction", + profile = profile_name.unwrap_or("legacy"), + resource_kind = "local_model_backend", + permission = "execute", + reason = "provider_unavailable"; + "Plugin resource access validation failed" ); - } - match config.max_latency_ms { - Some(0) => push( - "local.max_latency_ms", - "local.max_latency_ms must be greater than zero".into(), - ), - Some(latency) if latency > MAX_LOCAL_MODEL_LATENCY_MS => push( - "local.max_latency_ms", - format!("local.max_latency_ms must not exceed {MAX_LOCAL_MODEL_LATENCY_MS}"), - ), - _ => {} - } - for (field, value) in [ - ("local.model_id", config.model_id.as_deref()), - ("local.detector_profile", config.detector_profile.as_deref()), - ] { - if value.is_some_and(|value| value.trim().is_empty()) { - push(field, format!("{field} must be a non-empty string")); - } else if value.is_some_and(|value| value.len() > MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES) { - push( - field, - format!( - "{field} must not exceed {MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES} UTF-8 bytes" - ), + return Err(PluginError::RegistrationFailed( + "PII redaction local-model backend is unavailable in this runtime".to_string(), + )); + }; + log::info!( + target: "nemo_relay.plugin", + event = "plugin_resource_access_pending", + plugin_kind = "pii_redaction", + profile = profile_name.unwrap_or("legacy"), + resource_kind = "local_model_backend", + permission = "execute"; + "Plugin resource access validation started" + ); + let mut scoped_context = profile_name.map(|profile_name| { + PluginRegistrationContext::with_namespace( + ctx.qualify_name(&format!("{}/", profile_registration_prefix(profile_name))), + ) + }); + let provider_context = scoped_context.as_mut().unwrap_or(ctx); + match provider(config, provider_context) { + Ok(()) => { + if let Some(scoped_context) = scoped_context { + ctx.extend_registrations(scoped_context.into_registrations()); + } + log::info!( + target: "nemo_relay.plugin", + event = "plugin_resource_access_validated", + plugin_kind = "pii_redaction", + profile = profile_name.unwrap_or("legacy"), + resource_kind = "local_model_backend", + permission = "execute"; + "Plugin resource access validated" ); + Ok(()) + } + Err(error) => { + if let Some(scoped_context) = scoped_context { + let mut registrations = scoped_context.into_registrations(); + rollback_registrations(&mut registrations); + } + log::warn!( + target: "nemo_relay.plugin", + event = "plugin_resource_access_failed", + plugin_kind = "pii_redaction", + profile = profile_name.unwrap_or("legacy"), + resource_kind = "local_model_backend", + permission = "execute", + reason = "initialization_failed"; + "Plugin resource access validation failed" + ); + Err(error) } } - if config.target_paths.len() + config.target_path_patterns.len() > MAX_LOCAL_MODEL_TARGET_PATHS - { - push( - "local.target_paths", - format!( - "local.target_paths and local.target_path_patterns must contain at most {MAX_LOCAL_MODEL_TARGET_PATHS} entries in total" - ), - ); - } - if config - .target_paths - .iter() - .any(|path| path.len() > MAX_LOCAL_MODEL_TARGET_PATH_BYTES) - { - push( - "local.target_paths", - format!( - "local.target_paths entries must not exceed {MAX_LOCAL_MODEL_TARGET_PATH_BYTES} UTF-8 bytes" - ), - ); - } - if config - .target_paths - .iter() - .any(|path| !is_valid_json_pointer(path)) - { - push( - "local.target_paths", - "local.target_paths entries must be valid JSON pointers".into(), - ); - } - if config - .target_path_patterns - .iter() - .any(|path| path.len() > MAX_LOCAL_MODEL_TARGET_PATH_BYTES) - { - push( - "local.target_path_patterns", - format!( - "local.target_path_patterns entries must not exceed {MAX_LOCAL_MODEL_TARGET_PATH_BYTES} UTF-8 bytes" - ), - ); - } - if config - .target_path_patterns - .iter() - .any(|path| !is_valid_json_pointer_pattern(path)) - { - push( - "local.target_path_patterns", - "local.target_path_patterns entries must be valid JSON-pointer patterns".into(), - ); - } - if config - .min_score - .is_some_and(|score| !score.is_finite() || !(0.0..=1.0).contains(&score)) - { - push( - "local.min_score", - "local.min_score must be a finite number between 0 and 1".into(), - ); - } - if config.excluded_labels.len() > MAX_LOCAL_MODEL_EXCLUDED_LABELS { - push( - "local.excluded_labels", - format!( - "local.excluded_labels must contain at most {MAX_LOCAL_MODEL_EXCLUDED_LABELS} entries" - ), - ); - } - if config - .excluded_labels - .iter() - .any(|label| label.trim().is_empty() || label.len() > MAX_LOCAL_MODEL_LABEL_BYTES) - { - push( - "local.excluded_labels", - format!( - "local.excluded_labels entries must be non-empty and at most {MAX_LOCAL_MODEL_LABEL_BYTES} UTF-8 bytes" - ), - ); - } - if config - .excluded_labels - .iter() - .map(|label| label.trim()) - .collect::>() - .len() - != config.excluded_labels.len() - { - push( - "local.excluded_labels", - "local.excluded_labels must not contain duplicates".into(), - ); - } - if config - .replacement - .as_ref() - .is_some_and(|replacement| replacement.len() > MAX_LOCAL_MODEL_REPLACEMENT_BYTES) - { - push( - "local.replacement", - format!( - "local.replacement must not exceed {MAX_LOCAL_MODEL_REPLACEMENT_BYTES} UTF-8 bytes" - ), - ); - } - - violations } - -#[cfg(test)] -#[path = "../tests/unit/local_tests.rs"] -mod tests; diff --git a/crates/pii-redaction/tests/unit/component_tests.rs b/crates/pii-redaction/tests/unit/component_tests.rs index 61fb59c5e..cd7ad4026 100644 --- a/crates/pii-redaction/tests/unit/component_tests.rs +++ b/crates/pii-redaction/tests/unit/component_tests.rs @@ -29,11 +29,9 @@ use crate::codec::request::AnnotatedLlmRequest; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::plugin::{ ConfigPolicy, DiagnosticLevel, PluginComponentSpec, PluginConfig, PluginError, - PluginRegistrationContext, UnsupportedBehavior, WorkerInferenceDescriptor, - WorkerInferenceRegistration, WorkerInferenceRegistry, clear_plugin_configuration, + PluginRegistrationContext, UnsupportedBehavior, clear_plugin_configuration, ensure_builtin_plugins_registered, initialize_plugins_exact as initialize_plugins, - initialize_plugins_with_worker_inference, list_plugin_kinds, rollback_registrations, - validate_plugin_config, + list_plugin_kinds, rollback_registrations, validate_plugin_config, }; use futures::StreamExt; use nemo_relay::observability::atif::{AtifAgentInfo, AtifExporter}; @@ -142,32 +140,13 @@ fn top_level_policy_controls_component_diagnostics() { fn reset_runtime() { enable_operational_logs(); let _ = clear_plugin_configuration(); + crate::plugins::pii_redaction::component::clear_local_backend_provider().unwrap(); crate::shared_runtime::reset_runtime_owner_for_tests(); let context = global_context(); *context.write().unwrap() = NemoRelayContextState::new(); register_pii_redaction_component().unwrap(); } -struct WorkerInferenceGuard { - _registration: WorkerInferenceRegistration, -} - -fn register_test_worker_inference( - registry: &WorkerInferenceRegistry, - name: &str, - callback: impl Fn(Json, std::time::Duration) -> Result + Send + Sync + 'static, -) -> WorkerInferenceGuard { - let registration = registry - .register( - WorkerInferenceDescriptor::new(name, PII_DETECTION_CONTRACT).unwrap(), - Arc::new(callback), - ) - .unwrap(); - WorkerInferenceGuard { - _registration: registration, - } -} - fn setup_isolated_thread() { let stack = create_scope_stack(); set_thread_scope_stack(stack); @@ -317,48 +296,6 @@ impl LlmCodec for IdentifiedRequestCodec { } } -#[test] -fn raw_llm_paths_remain_usable_without_a_codec() { - let backend = crate::builtin::CompiledBuiltinBackend::new( - BuiltinBackendConfig { - action: "regex_replace".to_string(), - pattern: Some("sk-[A-Za-z0-9_-]+".to_string()), - replacement: Some("[REDACTED]".to_string()), - target_paths: vec!["/message".to_string()], - ..BuiltinBackendConfig::default() - }, - None, - ) - .unwrap(); - let sanitize_request = crate::builtin::llm_sanitize_request_callback(backend.clone()); - let sanitize_response = crate::builtin::llm_sanitize_response_callback(backend); - - let request = sanitize_request( - LlmRequest { - headers: serde_json::Map::new(), - content: json!({ - "message": "sk-request-secret", - "model": "sk-model-identifier" - }), - }, - no_codec_request_context(), - ) - .expect("raw request paths should not require a codec"); - assert_eq!(request.content["message"], "[REDACTED]"); - assert_eq!(request.content["model"], "sk-model-identifier"); - - let response = sanitize_response( - json!({ - "message": "sk-response-secret", - "model": "sk-model-identifier" - }), - no_codec_context(), - ) - .expect("raw response paths should not require a codec"); - assert_eq!(response["message"], "[REDACTED]"); - assert_eq!(response["model"], "sk-model-identifier"); -} - #[test] fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs() { let backend = crate::builtin::CompiledBuiltinBackend::new( @@ -1320,81 +1257,6 @@ fn profile_array_executes_every_profile_in_stable_array_order() { clear_plugin_configuration().unwrap(); } -#[test] -fn deterministic_and_local_model_profiles_compose_in_priority_order() { - let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); - reset_runtime(); - setup_isolated_thread(); - - let worker_inference = WorkerInferenceRegistry::default(); - let _registration = - register_test_worker_inference(&worker_inference, "contextual", |request, _| { - assert_eq!( - request["texts"][0]["text"], "Alice emailed [REDACTED]", - "the local worker must receive the deterministic profile's output" - ); - Ok(json!({ - "version": 1, - "detections": [{ - "text_id": 0, - "start_utf8": 0, - "end_utf8": 5, - "label": "GIVEN_NAME", - "score": 0.99 - }] - })) - }); - - futures::executor::block_on(initialize_plugins_with_worker_inference( - plugin_config(json!({ - "codec": "openai_chat", - "profiles": [ - { - "mode": "builtin", - "priority": 80, - "builtin": { - "action": "redact", - "detector": "email" - } - }, - { - "mode": "local_model", - "priority": 90, - "local": { - "backend": "contextual", - "target_paths": ["/message"] - } - } - ] - })), - worker_inference, - )) - .unwrap(); - - let events = capture_events("pii-profile-composition"); - event( - EmitMarkEventParams::builder() - .name("composed-profile-mark") - .data(json!({ - "message": "Alice emailed alice@example.com", - "region": "us-west-2" - })) - .build(), - ) - .unwrap(); - let captured = captured_events_snapshot(&events); - assert_eq!( - captured[0].data().unwrap(), - &json!({ - "message": "[REDACTED] emailed [REDACTED]", - "region": "us-west-2" - }) - ); - - deregister_subscriber("pii-profile-composition").unwrap(); - clear_plugin_configuration().unwrap(); -} - #[test] fn profile_array_rejects_legacy_fields_and_reports_profile_paths() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); @@ -1502,13 +1364,10 @@ fn local_profile_registrations_receive_generated_namespaces() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); - let worker_inference = WorkerInferenceRegistry::default(); - let _one = register_test_worker_inference(&worker_inference, "one", |_, _| { - Ok(json!({"version": 1, "detections": []})) - }); - let _two = register_test_worker_inference(&worker_inference, "two", |_, _| { - Ok(json!({"version": 1, "detections": []})) - }); + register_local_backend_provider(Arc::new(|_, ctx| { + ctx.register_mark_sanitize_guardrail("shared", 100, Arc::new(|_, fields| fields)) + })) + .unwrap(); let plugin = PiiRedactionPlugin; let config = json!({ @@ -1521,15 +1380,12 @@ fn local_profile_registrations_receive_generated_namespaces() { let Json::Object(config) = config else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_worker_inference( - Some("profiles::".into()), - worker_inference, - ); + let mut ctx = PluginRegistrationContext::with_namespace("profiles::"); futures::executor::block_on(plugin.register(&config, &mut ctx)).unwrap(); let mut registrations = ctx.into_registrations(); let registrations_debug = format!("{registrations:?}"); - assert!(registrations_debug.contains("profiles::profile_00000000000000000000/mark")); - assert!(registrations_debug.contains("profiles::profile_00000000000000000001/mark")); + assert!(registrations_debug.contains("profiles::profile_00000000000000000000/shared")); + assert!(registrations_debug.contains("profiles::profile_00000000000000000001/shared")); rollback_registrations(&mut registrations); assert!(registrations.is_empty()); } @@ -1540,6 +1396,12 @@ fn failed_later_profile_rolls_back_earlier_profile_registrations() { reset_runtime(); setup_isolated_thread(); + register_local_backend_provider(Arc::new(|_, _| { + Err(PluginError::RegistrationFailed( + "intentional profile failure".into(), + )) + })) + .unwrap(); let activation = futures::executor::block_on(initialize_plugins(plugin_config(json!({ "codec": "openai_chat", "profiles": [ @@ -1834,112 +1696,6 @@ fn validate_rejects_local_section_outside_local_mode() { })); } -#[test] -fn validate_rejects_invalid_local_model_worker_settings() { - let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); - reset_runtime(); - - let cases = [ - ( - json!({"mode": "local_model"}), - "local", - "required when mode = 'local_model'", - ), - ( - json!({"mode": "local_model", "local": {"backend": " "}}), - "local.backend", - "local.backend is required", - ), - ( - json!({ - "mode": "local_model", - "local": { - "backend": "x".repeat(MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES + 1) - } - }), - "local.backend", - "must not exceed", - ), - ( - json!({ - "mode": "local_model", - "local": {"backend": "worker", "allow_network": true} - }), - "local.allow_network", - "must not use network inference", - ), - ( - json!({ - "mode": "local_model", - "local": {"backend": "worker", "max_latency_ms": 0} - }), - "local.max_latency_ms", - "must be greater than zero", - ), - ( - json!({ - "mode": "local_model", - "local": {"backend": "worker", "max_latency_ms": 60001} - }), - "local.max_latency_ms", - "must not exceed 60000", - ), - ( - json!({ - "mode": "local_model", - "local": {"backend": "worker", "model_id": " "} - }), - "local.model_id", - "must be a non-empty string", - ), - ( - json!({ - "mode": "local_model", - "local": {"backend": "worker", "target_paths": ["message"]} - }), - "local.target_paths", - "valid JSON pointers", - ), - ( - json!({ - "mode": "local_model", - "local": { - "backend": "worker", - "replacement": "x".repeat(MAX_LOCAL_MODEL_REPLACEMENT_BYTES + 1) - } - }), - "local.replacement", - "must not exceed", - ), - ]; - - for (config, field, message) in cases { - let report = validate_plugin_config(&plugin_config(config)); - assert!(report.diagnostics.iter().any(|diagnostic| { - diagnostic.field.as_deref() == Some(field) && diagnostic.message.contains(message) - })); - } -} - -#[test] -fn validate_warns_when_local_model_inspects_every_string_leaf() { - let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); - reset_runtime(); - - let report = validate_plugin_config(&plugin_config(json!({ - "mode": "local_model", - "codec": "openai_chat", - "local": {"backend": "worker"} - }))); - - assert!(!report.has_errors(), "{:?}", report.diagnostics); - assert!(report.diagnostics.iter().any(|diagnostic| { - diagnostic.level == DiagnosticLevel::Warning - && diagnostic.code == "pii_redaction.local_model_all_paths" - && diagnostic.field.as_deref() == Some("local.target_paths") - })); -} - #[test] fn validate_rejects_builtin_mode_without_builtin_section() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); @@ -2070,127 +1826,65 @@ fn validate_rejects_unknown_builtin_detector() { } #[test] -fn local_backend_worker_inference_is_invoked_for_local_model_mode() { +fn local_backend_provider_is_invoked_for_local_model_mode() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); let called = Arc::new(AtomicBool::new(false)); let called_inner = Arc::clone(&called); - let worker_inference = WorkerInferenceRegistry::default(); - let _registration = - register_test_worker_inference(&worker_inference, "test-inference", move |request, _| { + register_local_backend_provider(Arc::new( + move |config, _ctx: &mut PluginRegistrationContext| { called_inner.store(true, Ordering::SeqCst); - assert_eq!(request["version"], 1); - Ok(json!({"version": 1, "detections": []})) - }); - setup_isolated_thread(); - futures::executor::block_on(initialize_plugins_with_worker_inference( - plugin_config(json!({ - "mode": "local_model", - "input": false, - "output": false, - "mark": false, - "tool_input": true, - "tool_output": false, - "local": {"backend": "test-inference"} - })), - worker_inference, + assert_eq!(config.mode, "local_model"); + Ok(()) + }, )) .unwrap(); - tool_call( - ToolCallParams::builder() - .name("test") - .args(json!({"text": "hello"})) - .build(), - ) - .unwrap(); - assert!(called.load(Ordering::SeqCst)); - clear_plugin_configuration().unwrap(); -} - -#[test] -fn local_backend_reports_missing_and_failed_worker_inference_initialization() { - let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); - reset_runtime(); let plugin = PiiRedactionPlugin; + let mut ctx = PluginRegistrationContext::with_namespace("test::"); let config = json!({ "mode": "local_model", - "input": false, - "output": false, - "mark": false, "tool_input": true, - "tool_output": false, - "local": {"backend": "missing"} }); let Json::Object(config) = config else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_namespace("missing::"); - let missing = futures::executor::block_on(plugin.register(&config, &mut ctx)) - .expect_err("missing worker inference should fail registration"); - assert!(missing.to_string().contains("unavailable")); - let worker_inference = WorkerInferenceRegistry::default(); - let _failed = register_test_worker_inference(&worker_inference, "failed", |_, _| { - Err(PluginError::RegistrationFailed( - "worker inference failed".into(), - )) - }); - let config = json!({ - "mode": "local_model", - "input": false, - "output": false, - "mark": false, - "tool_input": true, - "tool_output": false, - "local": {"backend": "failed"} - }); - let Json::Object(config) = config else { - panic!("component config must be object"); - }; - let mut ctx = - PluginRegistrationContext::with_worker_inference(Some("failed::".into()), worker_inference); - futures::executor::block_on(plugin.register(&config, &mut ctx)) - .expect("worker inference availability should be checked at registration"); - let mut registrations = ctx.into_registrations(); - rollback_registrations(&mut registrations); + futures::executor::block_on(plugin.register(&config, &mut ctx)).unwrap(); + + assert!(called.load(Ordering::SeqCst)); } #[test] -fn local_backend_rejects_worker_inference_with_incompatible_contract() { +fn local_backend_reports_missing_and_failed_provider_initialization() { let _guard = crate::plugins::pii_redaction::test_mutex().lock().unwrap(); reset_runtime(); - let worker_inference = WorkerInferenceRegistry::default(); - let _registration = worker_inference - .register( - WorkerInferenceDescriptor::new("embedding", "acme.embedding.v1").unwrap(), - Arc::new(|request, _| Ok(request)), - ) - .unwrap(); let plugin = PiiRedactionPlugin; - let Json::Object(config) = json!({ - "mode": "local_model", - "input": false, - "output": false, - "mark": false, - "tool_input": true, - "tool_output": false, - "local": {"backend": "embedding"} - }) else { + let config = json!({"mode": "local_model"}); + let Json::Object(config) = config else { panic!("component config must be object"); }; - let mut ctx = PluginRegistrationContext::with_worker_inference( - Some("mismatch::".into()), - worker_inference, - ); - - let error = futures::executor::block_on(plugin.register(&config, &mut ctx)) - .expect_err("PII must reject worker inference implementing another contract"); + let mut ctx = PluginRegistrationContext::with_namespace("missing::"); + let missing = futures::executor::block_on(plugin.register(&config, &mut ctx)) + .expect_err("missing local provider should fail registration"); + assert!(missing.to_string().contains("unavailable")); - assert!(error.to_string().contains("acme.embedding.v1")); - assert!(error.to_string().contains(PII_DETECTION_CONTRACT)); + register_local_backend_provider(Arc::new(|_, _| { + Err(PluginError::RegistrationFailed( + "provider initialization failed".into(), + )) + })) + .unwrap(); + let mut ctx = PluginRegistrationContext::with_namespace("failed::"); + let failed = futures::executor::block_on(plugin.register(&config, &mut ctx)) + .expect_err("failed local provider should fail registration"); + assert!( + failed + .to_string() + .contains("provider initialization failed") + ); } #[test] diff --git a/crates/pii-redaction/tests/unit/local_tests.rs b/crates/pii-redaction/tests/unit/local_tests.rs deleted file mode 100644 index 11336fdba..000000000 --- a/crates/pii-redaction/tests/unit/local_tests.rs +++ /dev/null @@ -1,971 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::atomic::{AtomicUsize, Ordering}; - -use nemo_relay::api::event::{BaseEvent, CategoryProfile, Event, EventSanitizeFields, MarkEvent}; -use nemo_relay::api::runtime::{LlmSanitizeRequestContext, LlmSanitizeResponseContext}; -use nemo_relay::codec::openai_responses::OpenAIResponsesCodec; -use nemo_relay::codec::request::AnnotatedLlmRequest; -use nemo_relay::codec::resolve::{ - ProviderSurface, request_codec as build_request_codec, response_codec as build_response_codec, -}; -use nemo_relay::codec::traits::LlmCodec; -use nemo_relay::plugin::{ - PluginRegistrationContext, WorkerInferenceDescriptor, WorkerInferenceRegistration, - WorkerInferenceRegistry, -}; -use serde_json::json; - -use super::*; - -struct WorkerInferenceGuard { - _registration: WorkerInferenceRegistration, -} - -struct IdentifiedRequestCodec { - identity: LlmCodecIdentity, -} - -impl LlmCodec for IdentifiedRequestCodec { - fn codec_identity(&self) -> LlmCodecIdentity { - self.identity.clone() - } - - fn decode(&self, request: &LlmRequest) -> nemo_relay::error::Result { - OpenAIResponsesCodec.decode(request) - } - - fn encode( - &self, - annotated: &AnnotatedLlmRequest, - original: &LlmRequest, - ) -> nemo_relay::error::Result { - OpenAIResponsesCodec.encode(annotated, original) - } -} - -fn worker_inference_context( - name: &'static str, - callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, -) -> (WorkerInferenceGuard, PluginRegistrationContext) { - let registry = WorkerInferenceRegistry::default(); - let registration = registry - .register( - WorkerInferenceDescriptor::new(name, PII_DETECTION_CONTRACT).unwrap(), - Arc::new(callback), - ) - .unwrap(); - ( - WorkerInferenceGuard { - _registration: registration, - }, - PluginRegistrationContext::with_worker_inference(None, registry), - ) -} - -fn backend( - name: &'static str, - callback: impl Fn(Json, Duration) -> PluginResult + Send + Sync + 'static, -) -> (WorkerInferenceGuard, CompiledLocalBackend) { - let (registration, ctx) = worker_inference_context(name, callback); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some(name.into()), - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - (registration, backend) -} - -fn alice_detector(request: Json, _timeout: Duration) -> PluginResult { - let mut detections = Vec::new(); - for item in request["texts"] - .as_array() - .expect("detection request should contain texts") - { - let text_id = item["id"].as_u64().expect("text id should be an integer"); - let text = item["text"].as_str().expect("text should be a string"); - for (start, value) in text.match_indices("Alice") { - detections.push(json!({ - "text_id": text_id, - "start_utf8": start, - "end_utf8": start + value.len(), - "label": "given_name", - "score": 0.99 - })); - } - } - Ok(json!({"version": 1, "detections": detections})) -} - -#[test] -fn applies_non_overlapping_utf8_byte_spans() { - let (_registration, backend) = backend("local-test-utf8", |_, _| { - Ok(json!({ - "version": 1, - "detections": [ - { - "text_id": 0, - "start_utf8": 0, - "end_utf8": 5, - "label": "given_name", - "score": 0.99 - }, - { - "text_id": 0, - "start_utf8": 6, - "end_utf8": 12, - "label": "surname", - "score": 0.98 - } - ] - })) - }); - - assert_eq!( - backend.sanitize_json(json!({"text": "José Rivera"})), - json!({"text": "[REDACTED] [REDACTED]"}) - ); -} - -#[test] -fn malformed_or_overlapping_spans_fail_closed_for_the_batch() { - let (_registration, backend) = backend("local-test-overlap", |_, _| { - Ok(json!({ - "version": 1, - "detections": [ - { - "text_id": 0, - "start_utf8": 0, - "end_utf8": 4, - "label": "name", - "score": 0.9 - }, - { - "text_id": 0, - "start_utf8": 3, - "end_utf8": 6, - "label": "name", - "score": 0.9 - } - ] - })) - }); - - assert_eq!( - backend.sanitize_json(json!({"first": "secret", "second": "safe"})), - json!({"first": "[REDACTED]", "second": "[REDACTED]"}) - ); -} - -#[test] -fn worker_inference_errors_fail_closed_without_changing_unselected_paths() { - let (_registration, ctx) = worker_inference_context("local-test-failure", |_, _| { - Err(PluginError::RegistrationFailed("boom".into())) - }); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-failure".into()), - target_paths: vec!["/selected".into()], - replacement: Some("[PRIVATE]".into()), - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - assert_eq!( - backend.sanitize_json(json!({ - "selected": "secret", - "unselected": "preserve" - })), - json!({ - "selected": "[PRIVATE]", - "unselected": "preserve" - }) - ); -} - -#[test] -fn batches_detection_requests_and_preserves_no_detection_values() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, backend) = backend("local-test-batching", move |request, _| { - observed.fetch_add(1, Ordering::SeqCst); - assert!(request["texts"].as_array().unwrap().len() <= MAX_BATCH_ITEMS); - Ok(json!({"version": 1, "detections": []})) - }); - let values = (0..(MAX_BATCH_ITEMS + 1)) - .map(|index| Json::String(format!("value-{index}"))) - .collect(); - - let sanitized = backend.sanitize_json(Json::Array(values)); - - assert_eq!(sanitized[0], "value-0"); - assert_eq!( - sanitized[MAX_BATCH_ITEMS], - format!("value-{MAX_BATCH_ITEMS}") - ); - assert_eq!(calls.load(Ordering::SeqCst), 2); -} - -#[test] -fn batches_multiple_event_roots_into_one_detection_request() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, backend) = backend("local-test-multi-root", move |request, _| { - observed.fetch_add(1, Ordering::SeqCst); - assert_eq!(request["texts"].as_array().unwrap().len(), 3); - Ok(json!({"version": 1, "detections": []})) - }); - - let sanitized = backend.sanitize_json_values(vec![ - json!({"message": "first"}), - json!({"name": "second"}), - json!({"trace": "third"}), - ]); - - assert_eq!( - sanitized, - vec![ - json!({"message": "first"}), - json!({"name": "second"}), - json!({"trace": "third"}), - ] - ); - assert_eq!(calls.load(Ordering::SeqCst), 1); -} - -#[test] -fn event_callback_batches_all_selected_fields_into_one_detection_request() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, backend) = backend("local-test-event-batching", move |request, _| { - observed.fetch_add(1, Ordering::SeqCst); - assert_eq!(request["texts"].as_array().unwrap().len(), 3); - Ok(json!({"version": 1, "detections": []})) - }); - let callback = event_sanitize_callback(backend, None); - let event = Event::Mark(MarkEvent::new( - BaseEvent::builder().name("mark").build(), - None, - None, - )); - - let sanitized = callback( - &event, - EventSanitizeFields { - data: Some(json!({"message": "first"})), - category_profile: Some(CategoryProfile::builder().subtype("second").build()), - metadata: Some(json!({"trace": "third"})), - }, - ); - - assert_eq!(sanitized.data.unwrap()["message"], "first"); - assert_eq!( - sanitized.category_profile.unwrap().subtype.as_deref(), - Some("second") - ); - assert_eq!(sanitized.metadata.unwrap()["trace"], "third"); - assert_eq!(calls.load(Ordering::SeqCst), 1); -} - -#[test] -fn latency_budget_applies_to_the_entire_payload() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, ctx) = - worker_inference_context("local-test-total-deadline", move |_, timeout| { - observed.fetch_add(1, Ordering::SeqCst); - std::thread::sleep(timeout + Duration::from_millis(5)); - Err(PluginError::RegistrationFailed("timed out".into())) - }); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-total-deadline".into()), - max_latency_ms: Some(10), - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - let values = (0..(MAX_BATCH_ITEMS + 1)) - .map(|index| Json::String(format!("value-{index}"))) - .collect(); - - let sanitized = backend.sanitize_json(Json::Array(values)); - - assert_eq!(calls.load(Ordering::SeqCst), 1); - assert!( - sanitized - .as_array() - .unwrap() - .iter() - .all(|value| value == "[REDACTED]") - ); -} - -#[test] -fn oversized_text_is_redacted_without_calling_the_registration() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, backend) = backend("local-test-oversized", move |_, _| { - observed.fetch_add(1, Ordering::SeqCst); - Ok(json!({"version": 1, "detections": []})) - }); - - assert_eq!( - backend.sanitize_json(Json::String("x".repeat(MAX_TEXT_BYTES + 1))), - Json::String("[REDACTED]".into()) - ); - assert_eq!(calls.load(Ordering::SeqCst), 0); -} - -#[test] -fn oversized_text_does_not_shift_later_inference_results() { - let (_registration, backend) = backend("local-test-oversized-middle", |_, _| { - Ok(json!({"version": 1, "detections": []})) - }); - - assert_eq!( - backend.sanitize_json(json!(["first", "x".repeat(MAX_TEXT_BYTES + 1), "third"])), - json!(["first", "[REDACTED]", "third"]) - ); -} - -#[test] -fn payload_count_limit_fails_closed_without_unbounded_inference_calls() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, backend) = backend("local-test-count-limit", move |_, _| { - observed.fetch_add(1, Ordering::SeqCst); - Ok(json!({"version": 1, "detections": []})) - }); - let values = (0..(MAX_TEXTS_PER_PAYLOAD + 1)) - .map(|index| Json::String(format!("value-{index}"))) - .collect(); - - let sanitized = backend.sanitize_json(Json::Array(values)); - - assert_eq!(sanitized[0], "value-0"); - assert_eq!(sanitized[MAX_TEXTS_PER_PAYLOAD], "[REDACTED]"); - assert_eq!( - calls.load(Ordering::SeqCst), - MAX_TEXTS_PER_PAYLOAD.div_ceil(MAX_BATCH_ITEMS) - ); -} - -#[test] -fn payload_byte_limit_fails_closed_after_the_bounded_prefix() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, backend) = backend("local-test-byte-limit", move |_, _| { - observed.fetch_add(1, Ordering::SeqCst); - Ok(json!({"version": 1, "detections": []})) - }); - let accepted = MAX_PAYLOAD_TEXT_BYTES / MAX_TEXT_BYTES; - let values = (0..=accepted) - .map(|_| Json::String("x".repeat(MAX_TEXT_BYTES))) - .collect(); - - let sanitized = backend.sanitize_json(Json::Array(values)); - - assert_eq!(sanitized[accepted - 1], "x".repeat(MAX_TEXT_BYTES)); - assert_eq!(sanitized[accepted], "[REDACTED]"); - assert_eq!( - calls.load(Ordering::SeqCst), - MAX_PAYLOAD_TEXT_BYTES.div_ceil(MAX_BATCH_BYTES) - ); -} - -#[test] -fn non_utf8_boundary_detection_fails_closed() { - let (_registration, backend) = backend("local-test-utf8-boundary", |_, _| { - Ok(json!({ - "version": 1, - "detections": [{ - "text_id": 0, - "start_utf8": 1, - "end_utf8": 2, - "label": "invalid", - "score": 1.0 - }] - })) - }); - - assert_eq!( - backend.sanitize_json(json!("é")), - Json::String("[REDACTED]".into()) - ); -} - -#[test] -fn local_policy_rejects_malformed_or_unbounded_values() { - let (_registration, ctx) = - worker_inference_context("local-test-policy-bounds", |request, _| Ok(request)); - - for (config, expected) in [ - ( - LocalBackendConfig { - backend: Some("x".repeat(MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES + 1)), - ..LocalBackendConfig::default() - }, - "local.backend", - ), - ( - LocalBackendConfig { - backend: Some("local-test-policy-bounds".into()), - target_paths: vec!["message".into()], - ..LocalBackendConfig::default() - }, - "valid JSON pointer", - ), - ( - LocalBackendConfig { - backend: Some("local-test-policy-bounds".into()), - target_paths: vec!["/bad~escape".into()], - ..LocalBackendConfig::default() - }, - "valid JSON pointer", - ), - ( - LocalBackendConfig { - backend: Some("local-test-policy-bounds".into()), - target_path_patterns: vec!["messages/*/content".into()], - ..LocalBackendConfig::default() - }, - "valid JSON-pointer pattern", - ), - ( - LocalBackendConfig { - backend: Some("local-test-policy-bounds".into()), - replacement: Some("x".repeat(MAX_LOCAL_MODEL_REPLACEMENT_BYTES + 1)), - ..LocalBackendConfig::default() - }, - "local.replacement", - ), - ( - LocalBackendConfig { - backend: Some("local-test-policy-bounds".into()), - model_id: Some("x".repeat(MAX_LOCAL_MODEL_PROVIDER_VALUE_BYTES + 1)), - ..LocalBackendConfig::default() - }, - "local.model_id", - ), - ( - LocalBackendConfig { - backend: Some("local-test-policy-bounds".into()), - min_score: Some(f64::NAN), - ..LocalBackendConfig::default() - }, - "local.min_score", - ), - ( - LocalBackendConfig { - backend: Some("local-test-policy-bounds".into()), - excluded_labels: vec!["NAME".into(), "NAME".into()], - ..LocalBackendConfig::default() - }, - "local.excluded_labels", - ), - ] { - let error = CompiledLocalBackend::new(config, None, &ctx) - .err() - .expect("invalid local policy should fail"); - assert!(error.to_string().contains(expected), "{error}"); - } -} - -#[test] -fn local_policy_accepts_root_and_escaped_json_pointers() { - assert!(is_valid_json_pointer("")); - assert!(is_valid_json_pointer("/nested/a~1b/~0value")); -} - -#[test] -fn target_path_patterns_match_one_segment_without_widening_exact_paths() { - let (_registration, ctx) = worker_inference_context("local-test-path-patterns", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-path-patterns".into()), - target_paths: vec!["/exact".into()], - target_path_patterns: vec!["/messages/*/content".into()], - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - assert_eq!( - backend.sanitize_json(json!({ - "exact": "Alice", - "messages": [ - {"content": "Alice", "name": "Alice"}, - {"content": "Alice"} - ], - "nested": {"messages": [{"content": "Alice"}]} - })), - json!({ - "exact": "[REDACTED]", - "messages": [ - {"content": "[REDACTED]", "name": "Alice"}, - {"content": "[REDACTED]"} - ], - "nested": {"messages": [{"content": "Alice"}]} - }) - ); -} - -#[test] -fn exact_paths_match_escaped_object_keys() { - let (_registration, ctx) = worker_inference_context("local-test-escaped-path", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-escaped-path".into()), - target_paths: vec!["/a~1b/~0name".into()], - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - assert_eq!( - backend.sanitize_json(json!({ - "a/b": {"~name": "Alice", "name": "Alice"}, - "a~1b": {"~name": "Alice"} - })), - json!({ - "a/b": {"~name": "[REDACTED]", "name": "Alice"}, - "a~1b": {"~name": "Alice"} - }) - ); -} - -#[test] -fn raw_request_paths_batch_headers_and_content_without_a_codec() { - let calls = Arc::new(AtomicUsize::new(0)); - let observed = Arc::clone(&calls); - let (_registration, ctx) = - worker_inference_context("local-test-raw-request", move |request, timeout| { - observed.fetch_add(1, Ordering::SeqCst); - alice_detector(request, timeout) - }); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-raw-request".into()), - target_paths: vec!["/headers/x-user".into(), "/message".into()], - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - let sanitized = llm_sanitize_request_callback(backend)( - LlmRequest { - headers: Map::from_iter([("x-user".into(), json!("Alice"))]), - content: json!({"message": "Hello Alice", "model": "Alice-model"}), - }, - LlmSanitizeRequestContext::default(), - ) - .expect("raw request paths should not require a codec"); - - assert_eq!(sanitized.headers["x-user"], "[REDACTED]"); - assert_eq!(sanitized.content["message"], "Hello [REDACTED]"); - assert_eq!(sanitized.content["model"], "Alice-model"); - assert_eq!(calls.load(Ordering::SeqCst), 1); -} - -#[test] -fn raw_response_paths_work_without_a_codec() { - let (_registration, ctx) = worker_inference_context("local-test-raw-response", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-raw-response".into()), - target_paths: vec!["/message".into()], - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - let sanitized = llm_sanitize_response_callback(backend)( - json!({"message": "Hello Alice", "model": "Alice-model"}), - LlmSanitizeResponseContext::default(), - ) - .expect("raw response paths should not require a codec"); - - assert_eq!(sanitized["message"], "Hello [REDACTED]"); - assert_eq!(sanitized["model"], "Alice-model"); -} - -#[test] -fn request_codec_classifies_only_normalized_content_patterns() { - let (_registration, ctx) = - worker_inference_context("local-test-openai-request", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-openai-request".into()), - target_path_patterns: vec![ - "/messages/*/content".into(), - "/messages/*/content/*/text".into(), - ], - ..LocalBackendConfig::default() - }, - Some("openai_chat".into()), - &ctx, - ) - .unwrap(); - let request = LlmRequest { - headers: serde_json::Map::new(), - content: json!({ - "model": "Alice-model", - "trace_id": "Alice-trace", - "messages": [ - {"role": "system", "content": "Keep this policy"}, - { - "role": "user", - "content": [ - {"type": "text", "text": "Email Alice"}, - {"type": "image_url", "image_url": {"url": "https://Alice.invalid"}} - ] - } - ] - }), - }; - let codec = build_request_codec(ProviderSurface::OpenAIChat); - let annotated = codec - .decode(&request) - .expect("OpenAI request should decode"); - let sanitized_annotated = - sanitize_serializable(&backend, annotated).expect("annotated request should sanitize"); - codec - .encode(&sanitized_annotated, &request) - .expect("sanitized OpenAI request should encode"); - - let sanitized = llm_sanitize_request_callback(backend)( - request, - LlmSanitizeRequestContext::for_request_codec(Some(codec)), - ) - .expect("valid request should remain observable"); - - assert_eq!(sanitized.content["model"], "Alice-model"); - assert_eq!(sanitized.content["trace_id"], "Alice-trace"); - assert_eq!( - sanitized.content["messages"][0]["content"], - "Keep this policy" - ); - assert_eq!( - sanitized.content["messages"][1]["content"][0]["text"], - "Email [REDACTED]" - ); - assert_eq!( - sanitized.content["messages"][1]["content"][1]["image_url"]["url"], - "https://Alice.invalid" - ); -} - -#[test] -fn request_uses_the_active_codec_instead_of_the_legacy_fallback() { - let (_registration, ctx) = - worker_inference_context("local-test-active-request-codec", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-active-request-codec".into()), - target_path_patterns: vec!["/messages/*/content".into()], - ..LocalBackendConfig::default() - }, - Some("openai_chat".into()), - &ctx, - ) - .unwrap(); - let sanitize = llm_sanitize_request_callback(backend); - let request = || LlmRequest { - headers: serde_json::Map::new(), - content: json!({ - "model": "test-model", - "input": [{"role": "user", "content": "Email Alice"}] - }), - }; - - for codec in [ - Arc::new(IdentifiedRequestCodec { - identity: LlmCodecIdentity::Runtime("test.responses.v1".into()), - }) as Arc, - Arc::new(IdentifiedRequestCodec { - identity: LlmCodecIdentity::Opaque, - }) as Arc, - ] { - let sanitized = sanitize( - request(), - LlmSanitizeRequestContext::for_request_codec(Some(codec)), - ) - .expect("active runtime codecs should remain usable"); - assert_eq!(sanitized.content["input"][0]["content"], "Email [REDACTED]"); - } -} - -#[test] -fn response_codec_classifies_message_content_without_touching_identity_fields() { - let (_registration, ctx) = - worker_inference_context("local-test-openai-response", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-openai-response".into()), - target_path_patterns: vec!["/message".into(), "/message/*/text".into()], - ..LocalBackendConfig::default() - }, - Some("openai_chat".into()), - &ctx, - ) - .unwrap(); - let response = json!({ - "id": "Alice-response", - "model": "Alice-model", - "choices": [{ - "index": 0, - "message": {"role": "assistant", "content": "Hello Alice"}, - "finish_reason": "stop" - }], - "vendor_trace": "Alice-trace" - }); - - let sanitized = backend - .sanitize_response_with_codec( - build_response_codec(ProviderSurface::OpenAIChat).as_ref(), - ProviderSurface::OpenAIChat, - response, - ) - .expect("configured codec should sanitize the response"); - - assert_eq!(sanitized["id"], "Alice-response"); - assert_eq!(sanitized["model"], "Alice-model"); - assert_eq!(sanitized["vendor_trace"], "Alice-trace"); - assert_eq!( - sanitized["choices"][0]["message"]["content"], - "Hello [REDACTED]" - ); -} - -#[test] -fn request_codec_failure_omits_the_observable_body() { - let (_registration, ctx) = - worker_inference_context("local-test-invalid-openai-request", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-invalid-openai-request".into()), - target_path_patterns: vec!["/messages/*/content".into()], - replacement: Some("[PRIVATE]".into()), - ..LocalBackendConfig::default() - }, - Some("openai_chat".into()), - &ctx, - ) - .unwrap(); - let request = LlmRequest { - headers: serde_json::Map::from_iter([( - "x-provider-id".into(), - Json::String("preserve-header".into()), - )]), - content: json!({ - "messages": "Alice cannot be decoded as an OpenAI message list", - "vendor_trace": "Alice-trace" - }), - }; - - let sanitized = llm_sanitize_request_callback(backend)( - request, - LlmSanitizeRequestContext::for_request_codec(Some(build_request_codec( - ProviderSurface::OpenAIChat, - ))), - ); - - assert!(sanitized.is_none()); -} - -#[test] -fn request_codec_ambiguous_multi_message_edit_fails_closed() { - let (_registration, ctx) = - worker_inference_context("local-test-ambiguous-openai-request", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-ambiguous-openai-request".into()), - target_path_patterns: vec!["/messages/*/content".into()], - replacement: Some("[PRIVATE]".into()), - ..LocalBackendConfig::default() - }, - Some("openai_chat".into()), - &ctx, - ) - .unwrap(); - let request = LlmRequest { - headers: serde_json::Map::new(), - content: json!({ - "messages": [ - {"role": "system", "content": "Alice owns this policy"}, - {"role": "user", "content": "Email Alice"} - ] - }), - }; - - let sanitized = llm_sanitize_request_callback(backend)( - request, - LlmSanitizeRequestContext::for_request_codec(Some(build_request_codec( - ProviderSurface::OpenAIChat, - ))), - ); - - assert!(sanitized.is_none()); -} - -#[test] -fn response_codec_failure_omits_the_observable_payload() { - let (_registration, ctx) = - worker_inference_context("local-test-invalid-openai-response", alice_detector); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-invalid-openai-response".into()), - target_path_patterns: vec!["/message".into()], - replacement: Some("[PRIVATE]".into()), - ..LocalBackendConfig::default() - }, - Some("openai_chat".into()), - &ctx, - ) - .unwrap(); - let response = json!({ - "choices": "Alice cannot be decoded as an OpenAI response list", - "vendor_trace": "Alice-trace" - }); - - let sanitized = llm_sanitize_response_callback(backend)( - response, - LlmSanitizeResponseContext::for_response_codec(Some(build_response_codec( - ProviderSurface::OpenAIChat, - ))), - ); - - assert!(sanitized.is_none()); -} - -#[test] -fn host_policy_applies_score_threshold_and_label_exclusions() { - let (_registration, ctx) = worker_inference_context("local-test-detection-policy", |_, _| { - Ok(json!({ - "version": 1, - "detections": [ - { - "text_id": 0, - "start_utf8": 0, - "end_utf8": 5, - "label": "LOW_SCORE", - "score": 0.49 - }, - { - "text_id": 0, - "start_utf8": 6, - "end_utf8": 11, - "label": "PRESERVE", - "score": 0.99 - }, - { - "text_id": 0, - "start_utf8": 12, - "end_utf8": 17, - "label": "REDACT", - "score": 0.99 - } - ] - })) - }); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-detection-policy".into()), - min_score: Some(0.5), - excluded_labels: vec!["PRESERVE".into()], - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - assert_eq!( - backend.sanitize_json(json!("first next1 final")), - json!("first next1 [REDACTED]") - ); -} - -#[test] -fn validates_filtered_detections_before_applying_host_policy() { - let (_registration, ctx) = - worker_inference_context("local-test-filtered-invalid-span", |_, _| { - Ok(json!({ - "version": 1, - "detections": [{ - "text_id": 0, - "start_utf8": 0, - "end_utf8": 999, - "label": "LOW_SCORE", - "score": 0.1 - }] - })) - }); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-filtered-invalid-span".into()), - min_score: Some(0.5), - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - assert_eq!( - backend.sanitize_json(json!("preserve without detections")), - json!("[REDACTED]") - ); -} - -#[test] -fn enforces_detection_limit_for_each_text() { - let (_registration, ctx) = - worker_inference_context("local-test-per-text-detection-limit", |_, _| { - let detections = (0..=MAX_DETECTIONS_PER_TEXT) - .map(|index| { - json!({ - "text_id": 0, - "start_utf8": index, - "end_utf8": index + 1, - "label": "NAME", - "score": 0.9 - }) - }) - .collect::>(); - Ok(json!({"version": 1, "detections": detections})) - }); - let backend = CompiledLocalBackend::new( - LocalBackendConfig { - backend: Some("local-test-per-text-detection-limit".into()), - ..LocalBackendConfig::default() - }, - None, - &ctx, - ) - .unwrap(); - - assert_eq!( - backend.sanitize_json(json!(["x".repeat(MAX_DETECTIONS_PER_TEXT + 1), "second"])), - json!(["[REDACTED]", "[REDACTED]"]) - ); -} diff --git a/crates/pii-redaction/tests/worker_detection_tests.rs b/crates/pii-redaction/tests/worker_detection_tests.rs deleted file mode 100644 index d402f5f12..000000000 --- a/crates/pii-redaction/tests/worker_detection_tests.rs +++ /dev/null @@ -1,360 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! End-to-end coverage for worker-backed local-model PII redaction. - -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::{Arc, Mutex, OnceLock}; - -use nemo_relay::api::event::Event; -use nemo_relay::api::llm::{LlmCallExecuteParams, LlmRequest, llm_call_execute}; -use nemo_relay::api::runtime::LlmExecutionNextFn; -use nemo_relay::api::scope::{EmitMarkEventParams, event}; -use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber}; -use nemo_relay::codec::openai_chat::OpenAIChatCodec; -use nemo_relay::codec::traits::LlmResponseCodec; -use nemo_relay::plugin::dynamic::{ - DynamicPluginActivationSpec, DynamicPluginKind, PluginHostActivation, -}; -use nemo_relay::plugin::{PluginComponentSpec, PluginConfig, clear_plugin_configuration}; -use nemo_relay_pii_redaction::component::{ - PII_DETECTION_CONTRACT, PII_REDACTION_PLUGIN_KIND, register_pii_redaction_component, -}; -use serde_json::{Map, json}; -use tempfile::TempDir; - -static WORKER_PII_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); - -#[tokio::test(flavor = "multi_thread")] -async fn worker_detection_sanitizes_events_and_is_removed_after_host_clear() { - let _guard = WORKER_PII_TEST_LOCK.lock().await; - let _ = clear_plugin_configuration(); - register_pii_redaction_component().expect("PII component should register"); - let worker_binary = build_fixture_worker(); - let (_manifest_dir, manifest_ref) = write_worker_manifest(&worker_binary); - - let plugin_config = PluginConfig { - version: 1, - components: vec![PluginComponentSpec { - kind: PII_REDACTION_PLUGIN_KIND.into(), - enabled: true, - config: Map::from_iter([ - ("mode".into(), json!("local_model")), - ("codec".into(), json!("openai_chat")), - ("input".into(), json!(true)), - ("output".into(), json!(true)), - ("tool_input".into(), json!(false)), - ("tool_output".into(), json!(false)), - ("mark".into(), json!(true)), - ( - "local".into(), - json!({ - "backend": "fixture_worker/fixture_local_model", - "target_paths": ["/message"], - "target_path_patterns": [ - "/messages/*/content", - "/message" - ] - }), - ), - ]), - }], - policy: Default::default(), - }; - let (activation, report) = PluginHostActivation::activate( - plugin_config, - [DynamicPluginActivationSpec { - plugin_id: "fixture_worker".into(), - kind: DynamicPluginKind::Worker, - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([("worker_inference_only".into(), json!(true))]), - }], - ) - .await - .expect("worker and PII component should activate together"); - assert!(!report.has_errors()); - let worker_inference = activation.worker_inference_registry(); - assert!( - worker_inference - .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT,) - .is_ok() - ); - - let events = Arc::new(Mutex::new(Vec::::new())); - let captured = Arc::clone(&events); - let subscriber_name = "worker-pii-e2e"; - register_subscriber( - subscriber_name, - Arc::new(move |event| captured.lock().unwrap().push(event.clone())), - ) - .expect("test subscriber should register"); - - let callback_value = json!({ - "message": "keep PRIVATE hidden", - "unselected": "PRIVATE remains outside the configured path" - }); - event( - EmitMarkEventParams::builder() - .name("worker-pii") - .data(callback_value.clone()) - .build(), - ) - .expect("mark should emit"); - flush_subscribers().expect("sanitized event should flush"); - - { - let captured = events.lock().unwrap(); - assert_eq!(captured.len(), 1); - assert_eq!( - captured[0].data(), - Some(&json!({ - "message": "keep [REDACTED] hidden", - "unselected": "PRIVATE remains outside the configured path" - })) - ); - } - assert_eq!( - callback_value["message"], "keep PRIVATE hidden", - "sanitization must not mutate caller-owned JSON" - ); - - let callback_request = Arc::new(Mutex::new(None)); - let observed_request = Arc::clone(&callback_request); - let response = json!({ - "id": "chatcmpl-PRIVATE", - "model": "model-PRIVATE", - "choices": [{ - "index": 0, - "message": { - "role": "assistant", - "content": "answer for PRIVATE" - }, - "finish_reason": "stop" - }] - }); - let callback_response = response.clone(); - let callback: LlmExecutionNextFn = Arc::new(move |request| { - *observed_request.lock().unwrap() = Some(request.clone()); - let response = callback_response.clone(); - Box::pin(async move { Ok(response) }) - }); - let request = LlmRequest { - headers: Map::new(), - content: json!({ - "model": "model-PRIVATE", - "messages": [ - {"role": "system", "content": "policy"}, - {"role": "user", "content": "question from PRIVATE"} - ], - "vendor_trace": "trace-PRIVATE" - }), - }; - let response_codec: Arc = Arc::new(OpenAIChatCodec); - - let returned = llm_call_execute( - LlmCallExecuteParams::builder() - .name("openai") - .request(request.clone()) - .func(callback) - .response_codec(response_codec) - .build(), - ) - .await - .expect("LLM callback should complete"); - flush_subscribers().expect("LLM events should flush"); - - assert_eq!( - returned, response, - "sanitize guardrails must not rewrite callback values" - ); - assert_eq!( - callback_request.lock().unwrap().as_ref(), - Some(&request), - "sanitize guardrails must not rewrite provider requests" - ); - let captured = events.lock().unwrap(); - assert_eq!(captured.len(), 3); - assert_eq!( - captured[1].input().unwrap()["content"]["messages"][1]["content"], - "question from [REDACTED]" - ); - assert_eq!( - captured[1].input().unwrap()["content"]["model"], - "model-PRIVATE", - "model identifiers are outside the selected content paths" - ); - assert_eq!( - captured[1].input().unwrap()["content"]["vendor_trace"], - "trace-PRIVATE", - "provider metadata is outside the selected content paths" - ); - assert_eq!( - captured[2].output().unwrap()["choices"][0]["message"]["content"], - "answer for [REDACTED]" - ); - assert_eq!(captured[2].output().unwrap()["id"], "chatcmpl-PRIVATE"); - assert_eq!(captured[2].output().unwrap()["model"], "model-PRIVATE"); - drop(captured); - - deregister_subscriber(subscriber_name).expect("test subscriber should deregister"); - activation.clear().expect("plugin host should clear"); - assert!( - worker_inference - .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT,) - .is_err(), - "worker inference should not outlive its host activation" - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn worker_exit_during_sanitization_fails_closed_and_removes_inference() { - let _guard = WORKER_PII_TEST_LOCK.lock().await; - let _ = clear_plugin_configuration(); - register_pii_redaction_component().expect("PII component should register"); - let worker_binary = build_fixture_worker(); - let (_manifest_dir, manifest_ref) = write_worker_manifest(&worker_binary); - let plugin_config = PluginConfig { - version: 1, - components: vec![PluginComponentSpec { - kind: PII_REDACTION_PLUGIN_KIND.into(), - enabled: true, - config: Map::from_iter([ - ("mode".into(), json!("local_model")), - ("input".into(), json!(false)), - ("output".into(), json!(false)), - ("tool_input".into(), json!(false)), - ("tool_output".into(), json!(false)), - ("mark".into(), json!(true)), - ( - "local".into(), - json!({ - "backend": "fixture_worker/fixture_local_model", - "target_paths": ["/message"] - }), - ), - ]), - }], - policy: Default::default(), - }; - let (activation, report) = PluginHostActivation::activate( - plugin_config, - [DynamicPluginActivationSpec { - plugin_id: "fixture_worker".into(), - kind: DynamicPluginKind::Worker, - manifest_ref: manifest_ref.to_string_lossy().into_owned(), - environment_ref: None, - config: Map::from_iter([ - ("worker_inference_only".into(), json!(true)), - ("exit_in_local_model".into(), json!(true)), - ]), - }], - ) - .await - .expect("worker and PII component should activate together"); - assert!(!report.has_errors()); - let worker_inference = activation.worker_inference_registry(); - - let events = Arc::new(Mutex::new(Vec::::new())); - let captured = Arc::clone(&events); - let subscriber_name = "worker-pii-exit"; - register_subscriber( - subscriber_name, - Arc::new(move |event| captured.lock().unwrap().push(event.clone())), - ) - .expect("test subscriber should register"); - - event( - EmitMarkEventParams::builder() - .name("worker-pii-exit") - .data(json!({ - "message": "PRIVATE", - "unselected": "PRIVATE" - })) - .build(), - ) - .expect("worker failure must not block event emission"); - flush_subscribers().expect("fail-closed event should flush"); - assert_eq!( - events.lock().unwrap()[0].data(), - Some(&json!({ - "message": "[REDACTED]", - "unselected": "PRIVATE" - })) - ); - - deregister_subscriber(subscriber_name).expect("test subscriber should deregister"); - let error = activation - .clear() - .expect_err("stopped worker shutdown should be reported") - .to_string(); - assert!(error.contains("shutdown"), "{error}"); - assert!( - worker_inference - .resolve("fixture_worker/fixture_local_model", PII_DETECTION_CONTRACT,) - .is_err(), - "failed worker inference should not survive host teardown" - ); -} - -fn build_fixture_worker() -> PathBuf { - static FIXTURE_BINARY: OnceLock = OnceLock::new(); - FIXTURE_BINARY - .get_or_init(|| { - let manifest = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../core/tests/fixtures/worker_plugin/Cargo.toml"); - let target_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../target/worker-plugin-fixture/target"); - let status = Command::new("cargo") - .arg("build") - .arg("--quiet") - .arg("--locked") - .arg("--manifest-path") - .arg(&manifest) - .arg("--target-dir") - .arg(&target_dir) - .status() - .expect("fixture worker build should start"); - assert!(status.success(), "fixture worker build should succeed"); - let binary = target_dir.join("debug").join(format!( - "nemo-relay-worker-plugin-fixture{}", - std::env::consts::EXE_SUFFIX - )); - assert!(binary.exists(), "fixture worker binary should exist"); - binary - }) - .clone() -} - -fn write_worker_manifest(binary: &Path) -> (TempDir, PathBuf) { - let temp = TempDir::new().expect("manifest directory should be created"); - let manifest = temp.path().join("relay-plugin.toml"); - let contents = format!( - r#" -manifest_version = 1 - -[plugin] -id = "fixture_worker" -kind = "worker" - -[compat] -relay = "={version}" -worker_protocol = "grpc-v1" - -[defaults] -enabled = false - -[capabilities] -items = ["plugin_worker"] - -[load] -runtime = "rust" -entrypoint = {entrypoint:?} -"#, - version = env!("CARGO_PKG_VERSION"), - entrypoint = binary.to_string_lossy(), - ); - std::fs::write(&manifest, contents).expect("worker manifest should be written"); - (temp, manifest) -} diff --git a/crates/pii-redaction/workers/rampart/MANIFEST.in b/crates/pii-redaction/workers/rampart/MANIFEST.in deleted file mode 100644 index af375315e..000000000 --- a/crates/pii-redaction/workers/rampart/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -include config.schema.json -include relay-plugin.toml diff --git a/crates/pii-redaction/workers/rampart/README.md b/crates/pii-redaction/workers/rampart/README.md deleted file mode 100644 index 9a25154b5..000000000 --- a/crates/pii-redaction/workers/rampart/README.md +++ /dev/null @@ -1,161 +0,0 @@ - - -# Rampart PII Worker - -This optional manifest-backed Python worker runs the -[`nationaldesignstudio/rampart`](https://huggingface.co/nationaldesignstudio/rampart) -ONNX token classifier behind NeMo Relay's `pii_redaction.local_model` backend. -Relay owns field selection, event sanitization, replacement, deadlines, and -fail-closed behavior. The worker performs detector inference only. - -The worker runs as a local child process over Relay's `grpc-v1` protocol. Its -Python, ONNX Runtime, NumPy, tokenizer, and model-cache dependencies remain in a -Relay-managed virtual environment rather than the Relay host process. Process -isolation is not a security sandbox. - -## Install - -From this directory: - -```bash -uvx --from . nemo-relay-pii-rampart-prefetch -nemo-relay plugins add ./relay-plugin.toml -nemo-relay plugins enable nemo_relay.pii_rampart -``` - -If the worker package is already installed, run -`nemo-relay-pii-rampart-prefetch` directly. `plugins add` creates a separate -Relay-managed Python environment from the same source directory. - -Rampart activation is offline-only. It requires the pinned 14.7 MB model -snapshot to already exist in the Hugging Face cache. Model acquisition is an -explicit setup step and never occurs during activation or inference. - -`model_id` normally remains `nationaldesignstudio/rampart`. The worker rejects -other repository identifiers and revisions because its integrity manifest pins -one supported snapshot. To load that same snapshot from a local directory, set -`model_id` to an absolute path or use explicit `./`, `../`, or `~/` syntax so a -relative directory cannot shadow the repository identifier. The PII -component's optional `local.model_id` remains the logical -`nationaldesignstudio/rampart` identifier even when worker storage uses a local -path. - -On hosts with slow or restricted access to Hugging Face, populate the shared -cache before enabling the plugin: - -```bash -nemo-relay-pii-rampart-prefetch -``` - -Run that command as the same operating-system user that runs Relay. If -`cache_dir` is configured for the plugin, pass the same value with -`--cache-dir`. The prefetch command and activation both verify SHA-256 digests -for every runtime model and tokenizer file. A missing or modified file blocks -activation. - -## Configure - -Add worker settings to the `[[plugins.dynamic]]` record created by `plugins -add`: - -```toml -[plugins.dynamic.config] -local_files_only = true -max_windows_per_request = 128 -inference_batch_size = 16 -max_pending_requests = 8 -``` - -`inference_batch_size` is an upper bound. The worker batches short token -windows together but automatically reduces the batch width for longer windows -to bound ONNX intermediate memory. - -Add the PII component to the same `plugins.toml`: - -```toml -[[components]] -kind = "pii_redaction" -enabled = true - -[components.config] -codec = "openai_chat" - -[[components.config.profiles]] -mode = "builtin" -priority = 70 - -[components.config.profiles.builtin] -action = "redact" -detector = "email" - -[[components.config.profiles]] -mode = "builtin" -priority = 80 - -[components.config.profiles.builtin] -action = "redact" -detector = "credit_card" - -[[components.config.profiles]] -mode = "local_model" -priority = 90 - -[components.config.profiles.local] -backend = "nemo_relay.pii_rampart/detector" -model_id = "nationaldesignstudio/rampart" -detector_profile = "default" -allow_network = false -max_latency_ms = 5000 -min_score = 0.4 -replacement = "[REDACTED]" -target_path_patterns = [ - "/messages/*/content", - "/messages/*/content/*/text", - "/message", - "/message/*/text", -] -``` - -`allow_network = false` means worker inference is local. It does not sandbox -the worker. `local_files_only` must remain `true`; activation-time model -acquisition is not supported. - -Rampart is the contextual detector lane, not a replacement for deterministic -recognizers. Configure built-in PII profiles for structured values and the -local-model profile for names and contextual identifiers. Keep the local-model -profile limited to normalized content paths. Classifying every string leaf can -produce false positives on model names, region names, UUIDs, trace IDs, and -other machine identifiers. Relay, not the worker, applies `min_score` and -optional `excluded_labels` policy after validating the worker response. - -## Runtime Bounds - -- At most 64 texts and 64 KiB of UTF-8 text are accepted per detection request. -- Each text is limited to 16 KiB. -- Long inputs use overlapping 510-token windows, with 64 content tokens of - overlap. -- ONNX inference batches and total windows are bounded by worker configuration. -- Requests above the worker bounds return an error; the PII component then - fails closed for the affected batch. -- `max_latency_ms` is one total budget for all inference batches selected from - one payload. -- CPU inference is serialized per worker process. Host deadlines cancel the - RPC, while already-running native inference is allowed to finish before its - admission slot is released. -- Use a `max_latency_ms` of at least 5000 when the selected payload can approach - the 64 KiB detection-request limit. Smaller content-only payloads normally - complete much faster. Benchmark representative inputs on deployment - hardware before lowering the deadline. - -The default model supports English, Spanish, French, German, Italian, -Portuguese, and Dutch. Its model card documents weak recall for non-Latin -scripts and government identifiers. Do not treat this detector as a complete -security boundary. - -## Attribution - -See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md). The model is downloaded -only by the explicit prefetch command and is not redistributed by this package. diff --git a/crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md b/crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md deleted file mode 100644 index 40e22ec71..000000000 --- a/crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# Third-Party Notices - -This optional worker downloads and executes **Rampart**, published by -National Design Studio at -[`nationaldesignstudio/rampart`](https://huggingface.co/nationaldesignstudio/rampart). -The model and its training-data attribution are published under the -[Creative Commons Attribution 4.0 International -license](https://creativecommons.org/licenses/by/4.0/). - -The default worker configuration selects model revision -`b1993e4e68b082835b80ffc65acc03325ea2e501`. Model files are downloaded to the -operator's Hugging Face cache and are not distributed in the NeMo Relay source -or Python package. diff --git a/crates/pii-redaction/workers/rampart/config.schema.json b/crates/pii-redaction/workers/rampart/config.schema.json deleted file mode 100644 index 11019be45..000000000 --- a/crates/pii-redaction/workers/rampart/config.schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "NeMo Relay Rampart PII Worker", - "type": "object", - "additionalProperties": false, - "properties": { - "model_id": { - "type": "string", - "minLength": 1, - "maxLength": 1024, - "default": "nationaldesignstudio/rampart", - "description": "Pinned Rampart repository identifier or an explicit local directory containing the same verified snapshot." - }, - "revision": { - "type": "string", - "const": "b1993e4e68b082835b80ffc65acc03325ea2e501", - "default": "b1993e4e68b082835b80ffc65acc03325ea2e501" - }, - "cache_dir": { - "type": "string", - "minLength": 1, - "maxLength": 4096 - }, - "local_files_only": { - "type": "boolean", - "const": true, - "default": true - }, - "max_windows_per_request": { - "type": "integer", - "minimum": 1, - "maximum": 512, - "default": 128 - }, - "inference_batch_size": { - "type": "integer", - "minimum": 1, - "maximum": 64, - "default": 16, - "description": "Maximum number of token windows per ONNX call. The worker reduces this automatically to bound padded token volume." - }, - "max_pending_requests": { - "type": "integer", - "minimum": 1, - "maximum": 64, - "default": 8 - }, - "intra_op_threads": { - "type": "integer", - "minimum": 1, - "maximum": 64 - } - } -} diff --git a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py deleted file mode 100644 index 78305e3b1..000000000 --- a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Rampart detector worker for the NeMo Relay PII component.""" - -from .detector import DEFAULT_MODEL_ID, DEFAULT_MODEL_REVISION, RampartDetector, RampartSettings - -__all__ = [ - "DEFAULT_MODEL_ID", - "DEFAULT_MODEL_REVISION", - "RampartDetector", - "RampartSettings", -] diff --git a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py deleted file mode 100644 index fa5325c19..000000000 --- a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py +++ /dev/null @@ -1,572 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Bounded ONNX token-classification adapter for the Rampart PII model.""" - -from __future__ import annotations - -import hashlib -import json -import threading -from collections.abc import Iterator, Mapping -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Protocol - -import numpy as np # ty: ignore[unresolved-import] -import onnxruntime as ort # ty: ignore[unresolved-import] -from huggingface_hub import snapshot_download # ty: ignore[unresolved-import] -from tokenizers import Tokenizer # ty: ignore[unresolved-import] - -DEFAULT_MODEL_ID = "nationaldesignstudio/rampart" -DEFAULT_MODEL_REVISION = "b1993e4e68b082835b80ffc65acc03325ea2e501" -CONTRACT_VERSION = 1 -MAX_TEXTS_PER_REQUEST = 64 -MAX_TEXT_BYTES = 16 * 1024 -MAX_REQUEST_TEXT_BYTES = 64 * 1024 -MODEL_MAX_TOKENS = 512 -SPECIAL_TOKEN_COUNT = 2 -CONTENT_TOKEN_BUDGET = MODEL_MAX_TOKENS - SPECIAL_TOKEN_COUNT -WINDOW_OVERLAP_TOKENS = 64 -MAX_PADDED_TOKENS_PER_BATCH = MODEL_MAX_TOKENS -MAX_MODEL_REFERENCE_BYTES = 1024 -MAX_DETECTOR_PROFILE_BYTES = 1024 -MAX_CACHE_PATH_BYTES = 4096 - -_MODEL_FILE_SHA256 = { - "config.json": "003b84bbcd489f5e782fe5cad8f3249c3653ec880089abb1ccc398a0d895e3e6", - "onnx/model_q4.onnx": "9f27d24949b0581701071ea5ef522d77ccd3f50c525cc91eac4d265b0fc2afe5", - "special_tokens_map.json": "5d5b662e421ea9fac075174bb0688ee0d9431699900b90662acd44b2a350503a", - "tokenizer.json": "98ade711428b42a1b5343c403a73344535e92de8e19359cdb567ef34da210259", - "tokenizer_config.json": "0088a6f8bcdd4014184fb068b83ebb12896a9db2bb269a71f73de83fef24bceb", -} - - -@dataclass(frozen=True) -class RampartSettings: - """Activation-time settings for one Rampart worker.""" - - model_id: str = DEFAULT_MODEL_ID - revision: str = DEFAULT_MODEL_REVISION - cache_dir: str | None = None - local_files_only: bool = True - max_windows_per_request: int = 128 - inference_batch_size: int = 16 - max_pending_requests: int = 8 - intra_op_threads: int | None = None - - @classmethod - def from_config(cls, config: Any) -> RampartSettings: - """Parse and validate dynamic-plugin configuration.""" - if not isinstance(config, dict): - raise TypeError("plugin config must be a JSON object") - allowed = { - "model_id", - "revision", - "cache_dir", - "local_files_only", - "max_windows_per_request", - "inference_batch_size", - "max_pending_requests", - "intra_op_threads", - } - unknown = sorted(set(config) - allowed) - if unknown: - raise ValueError(f"unknown plugin config field(s): {', '.join(unknown)}") - - model_id = _bounded_string( - config.get("model_id", DEFAULT_MODEL_ID), - "model_id", - MAX_MODEL_REFERENCE_BYTES, - ) - revision = _bounded_string( - config.get("revision", DEFAULT_MODEL_REVISION), - "revision", - MAX_MODEL_REFERENCE_BYTES, - ) - if not _uses_explicit_model_path(model_id) and model_id != DEFAULT_MODEL_ID: - raise ValueError(f"model_id must be {DEFAULT_MODEL_ID!r} or an explicit local directory") - if revision != DEFAULT_MODEL_REVISION: - raise ValueError(f"revision must be the pinned Rampart revision {DEFAULT_MODEL_REVISION!r}") - cache_dir = config.get("cache_dir") - if cache_dir is not None: - cache_dir = _bounded_string(cache_dir, "cache_dir", MAX_CACHE_PATH_BYTES) - local_files_only = config.get("local_files_only", True) - if not isinstance(local_files_only, bool): - raise TypeError("local_files_only must be a boolean") - if not local_files_only: - raise ValueError("local_files_only must remain true; prefetch the pinned model before enabling the plugin") - - return cls( - model_id=model_id, - revision=revision, - cache_dir=cache_dir, - local_files_only=local_files_only, - max_windows_per_request=_bounded_integer( - config.get("max_windows_per_request", 128), - "max_windows_per_request", - 1, - 512, - ), - inference_batch_size=_bounded_integer( - config.get("inference_batch_size", 16), - "inference_batch_size", - 1, - 64, - ), - max_pending_requests=_bounded_integer( - config.get("max_pending_requests", 8), - "max_pending_requests", - 1, - 64, - ), - intra_op_threads=_optional_bounded_integer(config.get("intra_op_threads"), "intra_op_threads", 1, 64), - ) - - -@dataclass(frozen=True) -class _InputText: - text_id: int - text: str - - -@dataclass(frozen=True) -class _Window: - text_id: int - input_ids: tuple[int, ...] - token_type_ids: tuple[int, ...] - offsets: tuple[tuple[int, int] | None, ...] - - -@dataclass(frozen=True) -class _Span: - start: int - end: int - label: str - score: float - - -class _Tokenizer(Protocol): - def encode(self, sequence: str, add_special_tokens: bool = True) -> Any: ... - - def token_to_id(self, token: str) -> int | None: ... - - -class _Session(Protocol): - def get_inputs(self) -> list[Any]: ... - - def get_outputs(self) -> list[Any]: ... - - def run(self, output_names: list[str], input_feed: dict[str, np.ndarray[Any, Any]]) -> list[Any]: ... - - -class RampartDetector: - """Load one Rampart model and perform bounded, serialized inference.""" - - def __init__( - self, - settings: RampartSettings, - tokenizer: _Tokenizer, - session: _Session, - labels: dict[int, str], - ) -> None: - self.settings = settings - self._tokenizer = tokenizer - self._session = session - self._labels = labels - if set(labels) != set(range(len(labels))): - raise ValueError("Rampart label IDs must be contiguous from zero") - self._lock = threading.Lock() - self._cls_id = _required_token_id(tokenizer, "[CLS]") - self._sep_id = _required_token_id(tokenizer, "[SEP]") - self._pad_id = _required_token_id(tokenizer, "[PAD]") - self._validate_model_contract() - - @classmethod - def load(cls, settings: RampartSettings) -> RampartDetector: - """Resolve model files and initialize an optimized CPU session.""" - model_root = resolve_verified_model_root(settings) - config = json.loads((model_root / "config.json").read_text(encoding="utf-8")) - raw_labels = config.get("id2label") - if not isinstance(raw_labels, dict): - raise ValueError("Rampart config.json must contain an id2label object") - labels = {int(index): str(label) for index, label in raw_labels.items()} - if not labels or labels.get(0) != "O": - raise ValueError("Rampart label map must define label 0 as O") - - options = ort.SessionOptions() - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - options.inter_op_num_threads = 1 - if settings.intra_op_threads is not None: - options.intra_op_num_threads = settings.intra_op_threads - session = ort.InferenceSession( - str(model_root / "onnx" / "model_q4.onnx"), - sess_options=options, - providers=["CPUExecutionProvider"], - ) - tokenizer = Tokenizer.from_file(str(model_root / "tokenizer.json")) - detector = cls(settings, tokenizer, session, labels) - detector._detect_texts([_InputText(0, "warmup")]) - return detector - - def detect_request(self, request: Any) -> dict[str, Any]: - """Validate one detection request and return versioned UTF-8 spans.""" - texts, requested_model, profile = _parse_request(request) - if requested_model is not None and requested_model != DEFAULT_MODEL_ID: - raise ValueError(f"request model_id {requested_model!r} does not match loaded model {DEFAULT_MODEL_ID!r}") - if profile not in (None, "default"): - raise ValueError(f"unsupported detector_profile {profile!r}") - - with self._lock: - detections = self._detect_texts(texts) - return { - "version": CONTRACT_VERSION, - "detections": detections, - } - - def _validate_model_contract(self) -> None: - inputs = {entry.name for entry in self._session.get_inputs()} - expected_inputs = {"input_ids", "attention_mask", "token_type_ids"} - if inputs != expected_inputs: - raise ValueError(f"Rampart ONNX inputs must be {sorted(expected_inputs)}, got {sorted(inputs)}") - outputs = self._session.get_outputs() - if len(outputs) != 1 or outputs[0].name != "logits": - raise ValueError("Rampart ONNX model must expose one logits output") - - def _detect_texts(self, texts: list[_InputText]) -> list[dict[str, Any]]: - windows = self._build_windows(texts) - spans_by_text: dict[int, list[_Span]] = {item.text_id: [] for item in texts} - for batch in _inference_batches(windows, self.settings.inference_batch_size): - logits = self._infer(batch) - for window, window_logits in zip(batch, logits, strict=True): - spans_by_text[window.text_id].extend(self._decode_window(window, window_logits)) - - detections = [] - text_by_id = {item.text_id: item.text for item in texts} - for text_id, spans in spans_by_text.items(): - merged_spans = _merge_overlapping_spans(spans) - if not merged_spans: - continue - byte_offsets = _utf8_offsets(text_by_id[text_id]) - for span in merged_spans: - detections.append( - { - "text_id": text_id, - "start_utf8": byte_offsets[span.start], - "end_utf8": byte_offsets[span.end], - "label": span.label, - "score": span.score, - } - ) - return detections - - def _build_windows(self, texts: list[_InputText]) -> list[_Window]: - windows = [] - step = CONTENT_TOKEN_BUDGET - WINDOW_OVERLAP_TOKENS - for item in texts: - inference_text = item.text.replace("-", " ") - encoding = self._tokenizer.encode(inference_text, add_special_tokens=False) - ids = list(encoding.ids) - type_ids = list(encoding.type_ids) - offsets = list(encoding.offsets) - if not (len(ids) == len(type_ids) == len(offsets)): - raise ValueError("tokenizer returned inconsistent token metadata") - if any(start < 0 or end < start or end > len(inference_text) for start, end in offsets): - raise ValueError("tokenizer returned invalid character offsets") - for start in range(0, len(ids), step): - end = min(start + CONTENT_TOKEN_BUDGET, len(ids)) - windows.append( - _Window( - text_id=item.text_id, - input_ids=(self._cls_id, *ids[start:end], self._sep_id), - token_type_ids=(0, *type_ids[start:end], 0), - offsets=(None, *offsets[start:end], None), - ) - ) - if len(windows) > self.settings.max_windows_per_request: - raise ValueError( - f"request exceeded max_windows_per_request={self.settings.max_windows_per_request}" - ) - if end == len(ids): - break - return windows - - def _infer(self, windows: list[_Window]) -> np.ndarray[Any, np.dtype[np.float32]]: - max_length = max(len(window.input_ids) for window in windows) - shape = (len(windows), max_length) - input_ids = np.full(shape, self._pad_id, dtype=np.int64) - attention_mask = np.zeros(shape, dtype=np.int64) - token_type_ids = np.zeros(shape, dtype=np.int64) - for index, window in enumerate(windows): - length = len(window.input_ids) - input_ids[index, :length] = window.input_ids - attention_mask[index, :length] = 1 - token_type_ids[index, :length] = window.token_type_ids - result = self._session.run( - ["logits"], - { - "input_ids": input_ids, - "attention_mask": attention_mask, - "token_type_ids": token_type_ids, - }, - ) - logits = np.asarray(result[0], dtype=np.float32) - expected = (len(windows), max_length, len(self._labels)) - if logits.shape != expected: - raise ValueError(f"Rampart logits shape must be {expected}, got {logits.shape}") - if not np.isfinite(logits).all(): - raise ValueError("Rampart logits must contain only finite values") - return logits - - def _decode_window(self, window: _Window, logits: np.ndarray[Any, Any]) -> list[_Span]: - label_ids = np.argmax(logits, axis=-1) - maxima = np.max(logits, axis=-1) - scores = 1.0 / np.exp(logits - maxima[:, None]).sum(axis=-1) - spans = [] - current_label: str | None = None - current_start = 0 - current_end = 0 - current_score = 0.0 - current_count = 0 - - def finish() -> None: - nonlocal current_label, current_start, current_end, current_score, current_count - if current_label is not None: - score = current_score / current_count - spans.append(_Span(current_start, current_end, current_label, score)) - current_label = None - current_start = 0 - current_end = 0 - current_score = 0.0 - current_count = 0 - - for index, offset in enumerate(window.offsets): - if offset is None or offset[0] >= offset[1]: - finish() - continue - raw_label = self._labels.get(int(label_ids[index])) - score = float(scores[index]) - prefix, label = _split_bio_label(raw_label) - if label is None: - finish() - continue - if current_label is None or prefix == "B" or label != current_label: - finish() - current_label = label - current_start = offset[0] - current_end = offset[1] - current_score = score - current_count = 1 - else: - current_end = max(current_end, offset[1]) - current_score += score - current_count += 1 - finish() - return spans - - -def _inference_batches(windows: list[_Window], max_batch_size: int) -> Iterator[list[_Window]]: - ordered = sorted(windows, key=lambda window: len(window.input_ids)) - batch: list[_Window] = [] - max_tokens = 0 - for window in ordered: - next_max_tokens = max(max_tokens, len(window.input_ids)) - padded_tokens = (len(batch) + 1) * next_max_tokens - if batch and (len(batch) >= max_batch_size or padded_tokens > MAX_PADDED_TOKENS_PER_BATCH): - yield batch - batch = [] - max_tokens = 0 - batch.append(window) - max_tokens = max(max_tokens, len(window.input_ids)) - if batch: - yield batch - - -def _resolve_model_root(settings: RampartSettings) -> Path: - local_path = _explicit_model_root(settings.model_id) - if local_path is not None: - return local_path - resolved = snapshot_download( - settings.model_id, - revision=settings.revision, - cache_dir=settings.cache_dir, - allow_patterns=list(_MODEL_FILE_SHA256), - local_files_only=True, - ) - return Path(resolved) - - -def resolve_verified_model_root(settings: RampartSettings) -> Path: - """Resolve and verify the pinned model assets without loading ONNX Runtime.""" - model_root = _resolve_model_root(settings) - _verify_model_files(model_root) - return model_root - - -def prefetch_verified_model(cache_dir: str | None = None) -> Path: - """Download and verify the pinned Rampart assets outside plugin activation.""" - if cache_dir is not None: - cache_dir = _bounded_string(cache_dir, "cache_dir", MAX_CACHE_PATH_BYTES) - model_root = Path( - snapshot_download( - DEFAULT_MODEL_ID, - revision=DEFAULT_MODEL_REVISION, - cache_dir=cache_dir, - allow_patterns=list(_MODEL_FILE_SHA256), - local_files_only=False, - ) - ) - _verify_model_files(model_root) - return model_root - - -def _verify_model_files( - model_root: Path, - expected: Mapping[str, str] = _MODEL_FILE_SHA256, -) -> None: - for relative_path, expected_sha256 in expected.items(): - path = model_root / relative_path - if not path.is_file(): - raise ValueError(f"Rampart model is missing required file {relative_path!r}") - with path.open("rb") as model_file: - digest = hashlib.file_digest(model_file, "sha256").hexdigest() - if digest != expected_sha256: - raise ValueError(f"Rampart model file {relative_path!r} failed SHA-256 verification") - - -def _explicit_model_root(model_id: str) -> Path | None: - candidate = Path(model_id).expanduser() - if not _uses_explicit_model_path(model_id): - return None - if not candidate.is_dir(): - raise ValueError(f"local Rampart model directory does not exist: {model_id}") - return candidate.resolve() - - -def _uses_explicit_model_path(model_id: str) -> bool: - return Path(model_id).expanduser().is_absolute() or model_id.startswith(("./", "../", ".\\", "..\\", "~/", "~\\")) - - -def _parse_request(request: Any) -> tuple[list[_InputText], str | None, str | None]: - if not isinstance(request, dict): - raise TypeError("local-model request must be a JSON object") - allowed = {"version", "model_id", "detector_profile", "texts"} - unknown = sorted(set(request) - allowed) - if unknown: - raise ValueError(f"unknown local-model request field(s): {', '.join(unknown)}") - version = request.get("version") - if isinstance(version, bool) or version != CONTRACT_VERSION: - raise ValueError(f"local-model request version must be {CONTRACT_VERSION}") - model_id = request.get("model_id") - if model_id is not None: - model_id = _bounded_string(model_id, "model_id", MAX_MODEL_REFERENCE_BYTES) - profile = request.get("detector_profile") - if profile is not None: - profile = _bounded_string(profile, "detector_profile", MAX_DETECTOR_PROFILE_BYTES) - raw_texts = request.get("texts") - if not isinstance(raw_texts, list) or not raw_texts: - raise TypeError("texts must be a non-empty array") - if len(raw_texts) > MAX_TEXTS_PER_REQUEST: - raise ValueError(f"texts must contain at most {MAX_TEXTS_PER_REQUEST} items") - - texts = [] - seen_ids = set() - total_bytes = 0 - for item in raw_texts: - if not isinstance(item, dict) or set(item) != {"id", "text"}: - raise TypeError("each texts item must contain exactly id and text") - text_id = item["id"] - text = item["text"] - if isinstance(text_id, bool) or not isinstance(text_id, int) or not 0 <= text_id <= 2**32 - 1: - raise TypeError("text id must be an unsigned 32-bit integer") - if text_id in seen_ids: - raise ValueError(f"duplicate text id {text_id}") - if not isinstance(text, str): - raise TypeError("text must be a string") - text_bytes = len(text.encode("utf-8")) - if text_bytes > MAX_TEXT_BYTES: - raise ValueError(f"text {text_id} exceeds {MAX_TEXT_BYTES} UTF-8 bytes") - total_bytes += text_bytes - if total_bytes > MAX_REQUEST_TEXT_BYTES: - raise ValueError(f"request text exceeds {MAX_REQUEST_TEXT_BYTES} UTF-8 bytes") - seen_ids.add(text_id) - texts.append(_InputText(text_id, text)) - return texts, model_id, profile - - -def _split_bio_label(raw_label: str | None) -> tuple[str | None, str | None]: - if raw_label is None or raw_label == "O": - return None, None - if raw_label.startswith(("B-", "I-")) and len(raw_label) > 2: - return raw_label[0], raw_label[2:].upper() - return "B", raw_label.upper() - - -def _merge_overlapping_spans(spans: list[_Span]) -> list[_Span]: - merged: list[_Span] = [] - for span in sorted(spans, key=lambda item: (item.start, -item.end, -item.score, item.label)): - if ( - not merged - or span.start > merged[-1].end - or (span.start == merged[-1].end and span.label != merged[-1].label) - ): - merged.append(span) - continue - previous = merged[-1] - winner = _preferred_span(previous, span) - merged[-1] = _Span( - start=min(previous.start, span.start), - end=max(previous.end, span.end), - label=winner.label, - score=max(previous.score, span.score), - ) - return merged - - -def _preferred_span(left: _Span, right: _Span) -> _Span: - left_key = (left.score, left.end - left.start, left.label) - right_key = (right.score, right.end - right.start, right.label) - return left if left_key >= right_key else right - - -def _utf8_offsets(text: str) -> list[int]: - offsets = [0] - total = 0 - for character in text: - total += len(character.encode("utf-8")) - offsets.append(total) - return offsets - - -def _required_token_id(tokenizer: _Tokenizer, token: str) -> int: - token_id = tokenizer.token_to_id(token) - if token_id is None: - raise ValueError(f"tokenizer is missing required token {token}") - return token_id - - -def _nonempty_string(value: Any, name: str) -> str: - if not isinstance(value, str) or not value.strip(): - raise TypeError(f"{name} must be a non-empty string") - return value - - -def _bounded_string(value: Any, name: str, maximum_bytes: int) -> str: - value = _nonempty_string(value, name) - if len(value.encode("utf-8")) > maximum_bytes: - raise ValueError(f"{name} must not exceed {maximum_bytes} UTF-8 bytes") - return value - - -def _bounded_integer(value: Any, name: str, minimum: int, maximum: int) -> int: - if isinstance(value, bool) or not isinstance(value, int): - raise TypeError(f"{name} must be an integer") - if not minimum <= value <= maximum: - raise ValueError(f"{name} must be between {minimum} and {maximum}") - return value - - -def _optional_bounded_integer(value: Any, name: str, minimum: int, maximum: int) -> int | None: - if value is None: - return None - return _bounded_integer(value, name, minimum, maximum) diff --git a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/prefetch.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/prefetch.py deleted file mode 100644 index f75d6b213..000000000 --- a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/prefetch.py +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Prefetch the pinned Rampart model before enabling the worker.""" - -from __future__ import annotations - -import argparse -from collections.abc import Sequence - -from .detector import prefetch_verified_model - - -def main(argv: Sequence[str] | None = None) -> None: - """Download and verify the model in the configured Hugging Face cache.""" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--cache-dir", - help="Optional Hugging Face cache directory shared with the worker.", - ) - args = parser.parse_args(argv) - model_root = prefetch_verified_model(args.cache_dir) - print(f"Verified Rampart model at {model_root}") - - -if __name__ == "__main__": - main() diff --git a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/py.typed b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py b/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py deleted file mode 100644 index 5383a7cc9..000000000 --- a/crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py +++ /dev/null @@ -1,116 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Manifest entrypoint for the Rampart PII worker.""" - -from __future__ import annotations - -import asyncio -from typing import Any - -from nemo_relay_plugin import ConfigDiagnostic, DiagnosticLevel, Json, PluginContext, WorkerPlugin, serve_plugin - -from .detector import RampartDetector, RampartSettings, resolve_verified_model_root - -PII_DETECTION_CONTRACT = "nemo.relay.pii_detection.v1" - - -class _Admission: - def __init__(self, limit: int) -> None: - self._limit = limit - self._active = 0 - - def acquire(self) -> None: - if self._active >= self._limit: - raise RuntimeError("Rampart worker is at its pending-request limit") - self._active += 1 - - def release(self) -> None: - self._active -= 1 - - -class RampartWorker(WorkerPlugin): - """Expose Rampart inference through the PII detection contract.""" - - plugin_id = "nemo_relay.pii_rampart" - - def validate(self, config: Json) -> list[ConfigDiagnostic | dict[str, Any]]: - try: - settings = RampartSettings.from_config(config) - except (TypeError, ValueError) as error: - return [ - ConfigDiagnostic( - level=DiagnosticLevel.ERROR, - code="nemo_relay.pii_rampart.invalid_config", - component=self.plugin_id, - message=str(error), - ) - ] - try: - resolve_verified_model_root(settings) - except Exception: - return [ - ConfigDiagnostic( - level=DiagnosticLevel.ERROR, - code="nemo_relay.pii_rampart.model_unavailable", - component=self.plugin_id, - message=( - "the pinned Rampart model is unavailable or failed integrity " - "verification; prefetch it before enabling the plugin" - ), - ) - ] - return [] - - def register(self, ctx: PluginContext, config: Json) -> None: - settings = RampartSettings.from_config(config) - detector = RampartDetector.load(settings) - admission = _Admission(settings.max_pending_requests) - inference_slot = asyncio.Lock() - - async def detect(request: Json) -> Json: - admission.acquire() - native_started = False - - async def run_native() -> Json: - nonlocal native_started - async with inference_slot: - native_started = True - return await asyncio.to_thread(detector.detect_request, request) - - work = asyncio.create_task(run_native()) - release_on_completion = False - try: - return await asyncio.shield(work) - except asyncio.CancelledError: - release_on_completion = True - if not native_started: - work.cancel() - - def release_after_work(_task: asyncio.Task[Json]) -> None: - try: - _task.exception() - except asyncio.CancelledError: - pass - admission.release() - - work.add_done_callback(release_after_work) - raise - finally: - if not release_on_completion: - admission.release() - - ctx.register_worker_inference( - "detector", - PII_DETECTION_CONTRACT, - detect, - ) - - -async def main() -> None: - """Start the Relay-managed worker.""" - await serve_plugin(RampartWorker()) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/crates/pii-redaction/workers/rampart/pyproject.toml b/crates/pii-redaction/workers/rampart/pyproject.toml deleted file mode 100644 index f70d467e5..000000000 --- a/crates/pii-redaction/workers/rampart/pyproject.toml +++ /dev/null @@ -1,57 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -[build-system] -requires = ["setuptools>=77"] -build-backend = "setuptools.build_meta" - -[project] -name = "nemo-relay-pii-rampart" -version = "0.1.0" -description = "Optional Rampart detector worker for NeMo Relay PII redaction" -readme = "README.md" -requires-python = ">=3.11" -license = "Apache-2.0" -license-files = ["THIRD_PARTY_NOTICES.md"] -authors = [ - { name = "NVIDIA Corporation & Affiliates" }, -] -dependencies = [ - "huggingface-hub>=0.34,<2", - "nemo-relay-plugin>=0.7,<1.0", - "numpy>=1.26,<3", - "onnxruntime>=1.20,<2", - "tokenizers>=0.21,<1", -] - -[project.scripts] -nemo-relay-pii-rampart-prefetch = "nemo_relay_pii_rampart.prefetch:main" - -[project.optional-dependencies] -test = [ - "pytest>=8", -] - -[tool.setuptools.packages.find] -where = ["."] -include = ["nemo_relay_pii_rampart"] - -[tool.setuptools.package-data] -nemo_relay_pii_rampart = ["py.typed"] - -[tool.ruff] -line-length = 120 -target-version = "py311" - -[tool.ruff.format] -quote-style = "double" - -[tool.ruff.lint] -select = ["E", "F", "W", "I"] - -[tool.ruff.lint.isort] -known-first-party = ["nemo_relay_pii_rampart", "nemo_relay_plugin"] - -[tool.ty.analysis] -# The model dependencies live only in the worker's managed environment. -allowed-unresolved-imports = ["huggingface_hub", "numpy", "onnxruntime", "tokenizers"] diff --git a/crates/pii-redaction/workers/rampart/relay-plugin.toml b/crates/pii-redaction/workers/rampart/relay-plugin.toml deleted file mode 100644 index f6e5f1839..000000000 --- a/crates/pii-redaction/workers/rampart/relay-plugin.toml +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -manifest_version = 1 - -[plugin] -id = "nemo_relay.pii_rampart" -kind = "worker" - -[compat] -relay = ">=0.7,<1.0" -worker_protocol = "grpc-v1" - -[defaults] -enabled = false - -[capabilities] -items = ["plugin_worker", "config_schema"] - -[config_schema] -path = "config.schema.json" - -[source] -manifest_root = "." -artifact = "nemo_relay_pii_rampart/worker.py" - -[integrity] -sha256 = "sha256:96a3dcf534061db2b5f3146702f433fdb2a10d980230dff13f740cc74897cd2c" - -[load] -runtime = "python" -entrypoint = "nemo_relay_pii_rampart.worker:main" diff --git a/crates/pii-redaction/workers/rampart/tests/test_detector.py b/crates/pii-redaction/workers/rampart/tests/test_detector.py deleted file mode 100644 index d1d75a575..000000000 --- a/crates/pii-redaction/workers/rampart/tests/test_detector.py +++ /dev/null @@ -1,339 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import hashlib -from pathlib import Path -from types import SimpleNamespace -from typing import Any - -import numpy as np # ty: ignore[unresolved-import] -import pytest - -import nemo_relay_pii_rampart.detector as detector_module -from nemo_relay_pii_rampart.detector import ( - DEFAULT_MODEL_ID, - RampartDetector, - RampartSettings, - _explicit_model_root, - _merge_overlapping_spans, - _parse_request, - _Span, - _verify_model_files, - prefetch_verified_model, -) - - -class FakeTokenizer: - _tokens = {"[PAD]": 0, "[CLS]": 2, "[SEP]": 3} - - def token_to_id(self, token: str) -> int | None: - return self._tokens.get(token) - - def encode(self, sequence: str, add_special_tokens: bool = True) -> SimpleNamespace: - del add_special_tokens - words = sequence.split() - ids = [] - offsets = [] - cursor = 0 - for index, word in enumerate(words): - start = sequence.index(word, cursor) - end = start + len(word) - ids.append(10 + index) - offsets.append((start, end)) - cursor = end - return SimpleNamespace(ids=ids, type_ids=[0] * len(ids), offsets=offsets) - - -class FakeSession: - def __init__(self, label_ids: list[int], scores: list[float]) -> None: - self._label_ids = label_ids - self._scores = scores - - def get_inputs(self) -> list[SimpleNamespace]: - return [SimpleNamespace(name=name) for name in ("input_ids", "attention_mask", "token_type_ids")] - - def get_outputs(self) -> list[SimpleNamespace]: - return [SimpleNamespace(name="logits")] - - def run(self, output_names: list[str], input_feed: dict[str, np.ndarray]) -> list[np.ndarray]: - assert output_names == ["logits"] - shape = (*input_feed["input_ids"].shape, 5) - logits = np.full(shape, -10.0, dtype=np.float32) - logits[:, :, 0] = 10.0 - for token_index, (label_id, score) in enumerate(zip(self._label_ids, self._scores, strict=True), start=1): - logits[:, token_index, 0] = 0.0 - logits[:, token_index, label_id] = np.log(score / (1.0 - score) * 4.0) - return [logits] - - -class NonFiniteSession(FakeSession): - def run(self, output_names: list[str], input_feed: dict[str, np.ndarray]) -> list[np.ndarray]: - logits = super().run(output_names, input_feed)[0] - logits[0, 0, 0] = np.nan - return [logits] - - -class RecordingSession(FakeSession): - def __init__(self) -> None: - super().__init__([], []) - self.shapes: list[tuple[int, int]] = [] - - def run(self, output_names: list[str], input_feed: dict[str, np.ndarray]) -> list[np.ndarray]: - self.shapes.append(input_feed["input_ids"].shape) - return super().run(output_names, input_feed) - - -def detector( - label_ids: list[int], - scores: list[float], - *, - max_windows_per_request: int | None = None, -) -> RampartDetector: - config = {} if max_windows_per_request is None else {"max_windows_per_request": max_windows_per_request} - return RampartDetector( - RampartSettings.from_config(config), - FakeTokenizer(), - FakeSession(label_ids, scores), - {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, - ) - - -def test_settings_validate_unknown_and_bounded_fields() -> None: - settings = RampartSettings.from_config({}) - assert settings.model_id == DEFAULT_MODEL_ID - assert settings.local_files_only is True - with pytest.raises(ValueError, match="unknown plugin config"): - RampartSettings.from_config({"surprise": True}) - with pytest.raises(TypeError, match="max_pending_requests"): - RampartSettings.from_config({"max_pending_requests": True}) - with pytest.raises(ValueError, match="model_id"): - RampartSettings.from_config({"model_id": "x" * 1025}) - with pytest.raises(ValueError, match="explicit local directory"): - RampartSettings.from_config({"model_id": "other/model"}) - with pytest.raises(ValueError, match="pinned Rampart revision"): - RampartSettings.from_config({"revision": "main"}) - with pytest.raises(ValueError, match="prefetch"): - RampartSettings.from_config({"local_files_only": False}) - - -def test_model_files_are_verified_before_loading(tmp_path: Path) -> None: - model_file = tmp_path / "model.bin" - model_file.write_bytes(b"trusted model") - expected = {"model.bin": hashlib.sha256(b"trusted model").hexdigest()} - - _verify_model_files(tmp_path, expected) - model_file.write_bytes(b"modified model") - with pytest.raises(ValueError, match="SHA-256 verification"): - _verify_model_files(tmp_path, expected) - - -def test_model_file_verification_rejects_missing_files(tmp_path: Path) -> None: - with pytest.raises(ValueError, match="missing required file"): - _verify_model_files(tmp_path, {"missing.bin": "0" * 64}) - - -def test_prefetch_uses_the_pinned_snapshot_and_verifies_it( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - observed: dict[str, Any] = {} - - def snapshot(model_id: str, **kwargs: Any) -> str: - observed["model_id"] = model_id - observed.update(kwargs) - return str(tmp_path) - - monkeypatch.setattr(detector_module, "snapshot_download", snapshot) - monkeypatch.setattr( - detector_module, "_verify_model_files", lambda model_root: observed.setdefault("root", model_root) - ) - - assert prefetch_verified_model("/tmp/rampart-cache") == tmp_path - assert observed["model_id"] == DEFAULT_MODEL_ID - assert observed["revision"] == detector_module.DEFAULT_MODEL_REVISION - assert observed["local_files_only"] is False - assert observed["cache_dir"] == "/tmp/rampart-cache" - assert observed["root"] == tmp_path - - -def test_local_model_directories_require_explicit_path_syntax( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - model_root = tmp_path / "model" - model_root.mkdir() - assert _explicit_model_root(str(model_root)) == model_root.resolve() - settings = RampartSettings.from_config({"model_id": str(model_root)}) - assert settings.model_id == str(model_root) - - shadow = tmp_path / "nationaldesignstudio" / "rampart" - shadow.mkdir(parents=True) - monkeypatch.chdir(tmp_path) - assert _explicit_model_root(DEFAULT_MODEL_ID) is None - with pytest.raises(ValueError, match="does not exist"): - _explicit_model_root("./missing") - - -def test_request_validation_rejects_duplicate_ids_and_byte_overflow() -> None: - with pytest.raises(ValueError, match="duplicate text id"): - _parse_request( - { - "version": 1, - "texts": [ - {"id": 0, "text": "one"}, - {"id": 0, "text": "two"}, - ], - } - ) - with pytest.raises(ValueError, match="UTF-8 bytes"): - _parse_request({"version": 1, "texts": [{"id": 0, "text": "é" * 9000}]}) - with pytest.raises(ValueError, match="model_id"): - _parse_request( - { - "version": 1, - "model_id": "x" * 1025, - "texts": [{"id": 0, "text": ""}], - } - ) - with pytest.raises(ValueError, match="detector_profile"): - _parse_request( - { - "version": 1, - "detector_profile": "x" * 1025, - "texts": [{"id": 0, "text": ""}], - } - ) - - -def test_no_detection_does_not_allocate_utf8_offset_tables(monkeypatch: pytest.MonkeyPatch) -> None: - def unexpected_offsets(_text: str) -> list[int]: - raise AssertionError("UTF-8 offsets should be lazy when no spans were detected") - - monkeypatch.setattr(detector_module, "_utf8_offsets", unexpected_offsets) - assert detector([], []).detect_request( - { - "version": 1, - "texts": [{"id": 0, "text": "no private values"}], - } - ) == {"version": 1, "detections": []} - - -def test_detector_returns_utf8_byte_offsets_and_model_labels() -> None: - value = detector([1, 2], [0.99, 0.98]).detect_request({"version": 1, "texts": [{"id": 7, "text": "José Rivera"}]}) - assert value["version"] == 1 - assert len(value["detections"]) == 1 - detection = value["detections"][0] - assert detection["text_id"] == 7 - assert detection["start_utf8"] == 0 - assert detection["end_utf8"] == len("José Rivera".encode()) - assert detection["label"] == "GIVEN_NAME" - assert 0.9 <= detection["score"] <= 1.0 - - city = detector([3, 4], [0.99, 0.98]).detect_request({"version": 1, "texts": [{"id": 0, "text": "New York"}]}) - assert city["detections"][0]["label"] == "CITY" - - -def test_detector_rejects_model_and_profile_mismatch() -> None: - current = detector([], []) - with pytest.raises(ValueError, match="does not match loaded model"): - current.detect_request( - { - "version": 1, - "model_id": "other/model", - "texts": [{"id": 0, "text": ""}], - } - ) - with pytest.raises(ValueError, match="unsupported detector_profile"): - current.detect_request( - { - "version": 1, - "detector_profile": "strict", - "texts": [{"id": 0, "text": ""}], - } - ) - - -def test_local_model_path_keeps_the_logical_model_identity(tmp_path: Path) -> None: - current = RampartDetector( - RampartSettings.from_config({"model_id": str(tmp_path)}), - FakeTokenizer(), - FakeSession([], []), - {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, - ) - result = current.detect_request( - { - "version": 1, - "model_id": DEFAULT_MODEL_ID, - "texts": [{"id": 0, "text": ""}], - } - ) - assert result == {"version": 1, "detections": []} - - -def test_overlapping_window_spans_are_coalesced() -> None: - spans = _merge_overlapping_spans( - [ - _Span(0, 10, "GIVEN_NAME", 0.8), - _Span(5, 12, "SURNAME", 0.9), - _Span(20, 24, "PHONE", 0.7), - _Span(24, 28, "PHONE", 0.8), - _Span(28, 30, "TAX_ID", 0.9), - ] - ) - assert spans == [ - _Span(0, 12, "SURNAME", 0.9), - _Span(20, 28, "PHONE", 0.8), - _Span(28, 30, "TAX_ID", 0.9), - ] - - -def test_request_window_limit_is_enforced_before_inference() -> None: - current = detector([], [], max_windows_per_request=1) - text = " ".join(f"word{index}" for index in range(600)) - with pytest.raises(ValueError, match="max_windows_per_request"): - current.detect_request({"version": 1, "texts": [{"id": 0, "text": text}]}) - - -def test_inference_batches_short_windows_and_isolates_full_windows() -> None: - session = RecordingSession() - current = RampartDetector( - RampartSettings.from_config({"inference_batch_size": 16}), - FakeTokenizer(), - session, - {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, - ) - full_window = " ".join("word" for _ in range(detector_module.CONTENT_TOKEN_BUDGET)) - texts = [{"id": index, "text": "short"} for index in range(16)] - texts.extend( - [ - {"id": 16, "text": full_window}, - {"id": 17, "text": full_window}, - ] - ) - - assert current.detect_request({"version": 1, "texts": texts}) == { - "version": 1, - "detections": [], - } - assert session.shapes == [(16, 3), (1, detector_module.MODEL_MAX_TOKENS), (1, detector_module.MODEL_MAX_TOKENS)] - - -def test_detector_rejects_invalid_model_outputs() -> None: - with pytest.raises(ValueError, match="contiguous"): - RampartDetector( - RampartSettings(), - FakeTokenizer(), - FakeSession([], []), - {0: "O", 2: "B-GIVEN_NAME"}, - ) - - current = RampartDetector( - RampartSettings(), - FakeTokenizer(), - NonFiniteSession([], []), - {0: "O", 1: "B-GIVEN_NAME", 2: "I-GIVEN_NAME", 3: "B-CITY", 4: "I-CITY"}, - ) - with pytest.raises(ValueError, match="finite"): - current.detect_request({"version": 1, "texts": [{"id": 0, "text": "hello"}]}) diff --git a/crates/pii-redaction/workers/rampart/tests/test_worker.py b/crates/pii-redaction/workers/rampart/tests/test_worker.py deleted file mode 100644 index 74d5e4433..000000000 --- a/crates/pii-redaction/workers/rampart/tests/test_worker.py +++ /dev/null @@ -1,173 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import asyncio -import threading -from typing import Any, cast - -import pytest - -import nemo_relay_pii_rampart.worker as worker_module -from nemo_relay_pii_rampart.worker import RampartWorker -from nemo_relay_plugin import ConfigDiagnostic, PluginContext - - -class FakeContext: - def __init__(self) -> None: - self.callback: Any = None - - def register_worker_inference(self, name: str, contract: str, callback: Any) -> None: - assert name == "detector" - assert contract == "nemo.relay.pii_detection.v1" - self.callback = callback - - -class FakeDetector: - def __init__(self, started: threading.Event | None = None, release: threading.Event | None = None) -> None: - self.started = started - self.release = release - - def detect_request(self, request: Any) -> dict[str, Any]: - if self.started is not None: - self.started.set() - if self.release is not None: - self.release.wait(timeout=5) - return {"version": 1, "detections": [], "echo": request} - - -class FailingDetector: - def detect_request(self, request: Any) -> dict[str, Any]: - del request - raise RuntimeError("detector failed") - - -def test_worker_validation_reports_invalid_config(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(worker_module, "resolve_verified_model_root", lambda _settings: None) - worker = RampartWorker() - assert worker.validate({}) == [] - diagnostics = worker.validate({"max_pending_requests": "many"}) - assert len(diagnostics) == 1 - diagnostic = diagnostics[0] - assert isinstance(diagnostic, ConfigDiagnostic) - assert diagnostic.code == "nemo_relay.pii_rampart.invalid_config" - diagnostics = worker.validate({"local_files_only": False}) - assert len(diagnostics) == 1 - diagnostic = diagnostics[0] - assert isinstance(diagnostic, ConfigDiagnostic) - assert diagnostic.code == "nemo_relay.pii_rampart.invalid_config" - - -def test_worker_validation_reports_bounded_model_readiness_error( - monkeypatch: pytest.MonkeyPatch, -) -> None: - def fail(_settings: Any) -> None: - raise ValueError("/sensitive/cache/path/model_q4.onnx is corrupt") - - monkeypatch.setattr(worker_module, "resolve_verified_model_root", fail) - - diagnostics = RampartWorker().validate({}) - - assert len(diagnostics) == 1 - diagnostic = diagnostics[0] - assert isinstance(diagnostic, ConfigDiagnostic) - assert diagnostic.code == "nemo_relay.pii_rampart.model_unavailable" - assert "prefetch" in diagnostic.message - assert "/sensitive" not in diagnostic.message - - -def test_worker_registers_async_inference(monkeypatch: pytest.MonkeyPatch) -> None: - fake = FakeDetector() - monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: fake) - context = FakeContext() - RampartWorker().register(cast(PluginContext, context), {}) - - request = {"version": 1, "texts": [{"id": 0, "text": "hello"}]} - assert asyncio.run(context.callback(request)) == { - "version": 1, - "detections": [], - "echo": request, - } - - -def test_cancelled_callback_holds_admission_until_native_work_finishes(monkeypatch: pytest.MonkeyPatch) -> None: - started = threading.Event() - release = threading.Event() - fake = FakeDetector(started, release) - monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: fake) - context = FakeContext() - RampartWorker().register(cast(PluginContext, context), {"max_pending_requests": 1}) - - async def exercise() -> None: - first = asyncio.create_task(context.callback({"version": 1, "texts": [{"id": 0, "text": "one"}]})) - assert await asyncio.to_thread(started.wait, 1) - first.cancel() - with pytest.raises(asyncio.CancelledError): - await first - with pytest.raises(RuntimeError, match="pending-request limit"): - await context.callback({"version": 1, "texts": [{"id": 1, "text": "two"}]}) - - release.set() - for _ in range(100): - await asyncio.sleep(0.01) - try: - result = await context.callback({"version": 1, "texts": [{"id": 2, "text": "three"}]}) - except RuntimeError: - continue - assert result["version"] == 1 - return - pytest.fail("worker admission was not released after native work completed") - - asyncio.run(exercise()) - - -def test_cancelled_queued_callback_does_not_run_native_inference(monkeypatch: pytest.MonkeyPatch) -> None: - started = threading.Event() - release = threading.Event() - requests: list[int] = [] - - class RecordingDetector: - def detect_request(self, request: Any) -> dict[str, Any]: - requests.append(request["texts"][0]["id"]) - started.set() - release.wait(timeout=5) - return {"version": 1, "detections": []} - - monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: RecordingDetector()) - context = FakeContext() - RampartWorker().register(cast(PluginContext, context), {"max_pending_requests": 2}) - - async def exercise() -> None: - first = asyncio.create_task(context.callback({"version": 1, "texts": [{"id": 0, "text": "one"}]})) - assert await asyncio.to_thread(started.wait, 1) - second = asyncio.create_task(context.callback({"version": 1, "texts": [{"id": 1, "text": "two"}]})) - await asyncio.sleep(0) - second.cancel() - with pytest.raises(asyncio.CancelledError): - await second - - release.set() - await first - await context.callback({"version": 1, "texts": [{"id": 2, "text": "three"}]}) - - asyncio.run(exercise()) - assert requests == [0, 2] - - -def test_detector_failure_releases_admission(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(worker_module.RampartDetector, "load", lambda _settings: FailingDetector()) - context = FakeContext() - RampartWorker().register(cast(PluginContext, context), {"max_pending_requests": 1}) - - async def exercise() -> None: - for text_id in range(2): - with pytest.raises(RuntimeError, match="detector failed"): - await context.callback( - { - "version": 1, - "texts": [{"id": text_id, "text": "private"}], - } - ) - - asyncio.run(exercise()) diff --git a/crates/worker-proto/README.md b/crates/worker-proto/README.md index e657eb224..d6e0e21d4 100644 --- a/crates/worker-proto/README.md +++ b/crates/worker-proto/README.md @@ -33,8 +33,6 @@ tooling. from `v1` without generating protobuf code in a consumer project. - **Keep data ownership clear**: Carry Relay DTOs in JSON envelopes backed by `nemo-relay-types`; protobuf owns transport control flow. -- **Implement workers in another language**: Generate a `grpc-v1` service from - the protobuf when no maintained language-specific authoring SDK exists. ## What You Get @@ -43,9 +41,6 @@ tooling. clients, servers, services, and messages. - **JSON envelope helpers**: `json_envelope` and `decode_json_envelope` for serializing Relay DTOs into protocol payloads. -- **Language-neutral worker inference**: `WORKER_INFERENCE` carries a - versioned contract identifier plus component-owned request and response JSON - without coupling the host to the worker implementation language. ## Installation diff --git a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto index 9f9564e9b..75307e56d 100644 --- a/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto +++ b/crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto @@ -52,7 +52,6 @@ enum RegistrationSurface { MARK_SANITIZE_GUARDRAIL = 30; SCOPE_SANITIZE_START_GUARDRAIL = 31; SCOPE_SANITIZE_END_GUARDRAIL = 32; - WORKER_INFERENCE = 40; } enum LlmCodecKind { @@ -143,7 +142,7 @@ message Registration { RegistrationSurface surface = 2; int32 priority = 3; bool break_chain = 4; - string contract = 5; + reserved 5; } message InvokeRequest { @@ -159,7 +158,6 @@ message InvokeRequest { JsonEnvelope event = 10; ToolInvocation tool = 11; LlmInvocation llm = 12; - JsonEnvelope worker_inference = 13; } } diff --git a/crates/worker-proto/tests/proto_tests.rs b/crates/worker-proto/tests/proto_tests.rs index ba315566f..3b34ad9d1 100644 --- a/crates/worker-proto/tests/proto_tests.rs +++ b/crates/worker-proto/tests/proto_tests.rs @@ -4,8 +4,7 @@ //! Tests for stable worker protocol helpers and enum values. use nemo_relay_worker_proto::v1::{ - HandshakeRequest, HealthRequest, InvokeRequest, JsonEnvelope, Registration, - RegistrationSurface, ScopeType, + HandshakeRequest, HealthRequest, InvokeRequest, JsonEnvelope, RegistrationSurface, ScopeType, }; use nemo_relay_worker_proto::{WORKER_PROTOCOL_GRPC_V1, decode_json_envelope, json_envelope}; use prost::Message; @@ -42,29 +41,6 @@ fn registration_surface_values_are_stable() { assert_eq!(RegistrationSurface::MarkSanitizeGuardrail as i32, 30); assert_eq!(RegistrationSurface::ScopeSanitizeStartGuardrail as i32, 31); assert_eq!(RegistrationSurface::ScopeSanitizeEndGuardrail as i32, 32); - assert_eq!(RegistrationSurface::WorkerInference as i32, 40); - let encoded = Registration { - contract: "x".into(), - ..Default::default() - } - .encode_to_vec(); - assert_eq!(encoded, vec![42, 1, b'x']); - - let encoded = InvokeRequest { - surface: RegistrationSurface::WorkerInference as i32, - payload: Some( - nemo_relay_worker_proto::v1::invoke_request::Payload::WorkerInference(JsonEnvelope { - schema: "s".into(), - json: b"{}".to_vec(), - }), - ), - ..Default::default() - } - .encode_to_vec(); - assert_eq!( - encoded, - vec![32, 40, 106, 7, 10, 1, b's', 18, 2, b'{', b'}'] - ); } #[test] diff --git a/crates/worker/README.md b/crates/worker/README.md index 439139be9..1f5d81ce3 100644 --- a/crates/worker/README.md +++ b/crates/worker/README.md @@ -25,8 +25,7 @@ communicates with Relay through the versioned `grpc-v1` worker protocol. - **Isolate plugin code**: Run custom runtime behavior outside the Relay host process. - **Use typed registration APIs**: Implement `WorkerPlugin` and register - subscribers, guardrails, intercepts, or worker inference callbacks with - `PluginContext`. + subscribers, guardrails, or intercepts with `PluginContext`. - **Call the host runtime**: Emit marks, manage scopes, and invoke middleware continuations through `PluginRuntime`. - **Keep lifecycle managed**: Let Relay provide authenticated endpoints and @@ -86,29 +85,6 @@ Relay supplies the socket, activation ID, and authentication token through the worker environment. Use `serve_plugin` for Relay-spawned workers; explicit server configuration is intended for tests and custom launchers. -## Worker Inference - -A worker can expose contract-scoped inference to a first-party host component -without owning middleware policy: - -```rust -ctx.register_worker_inference( - "detector", - "acme.pii_detection.v1", - |request| async move { - Ok(serde_json::json!({ - "version": 1, - "detections": detect(request)? - })) - }, -); -``` - -The host publishes the callback as `/detector`. The consuming -component selects the exact contract and owns the request and response schema, -deadline, field selection, validation, and application of the result. The -worker callback should perform inference only. - ## Concurrency and Cancellation Unary and streaming callbacks run concurrently. Cancellation is cooperative: diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 75f0cff38..f2e523f4a 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -270,7 +270,6 @@ type LlmRequestFn = Arc< type LlmExecutionFn = Arc BoxFutureResult + Send + Sync>; type LlmStreamExecutionFn = Arc BoxFutureResult + Send + Sync>; -type WorkerInferenceFn = Arc BoxFutureResult + Send + Sync>; #[derive(Default)] struct WorkerHandlers { @@ -290,7 +289,6 @@ struct WorkerHandlers { llm_requests: HashMap, llm_executions: HashMap, llm_stream_executions: HashMap, - worker_inference: HashMap, } /// Registration context passed to [`WorkerPlugin::register`]. @@ -332,23 +330,6 @@ impl PluginContext { .insert(name.into(), Arc::new(callback)); } - /// Registers named worker inference for a versioned host contract. - /// - /// The callback receives and returns versioned JSON data owned by the - /// consuming host component. It does not register middleware or decide - /// which runtime fields are sanitized. - pub fn register_worker_inference(&mut self, name: &str, contract: &str, callback: F) - where - F: Fn(Json) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + 'static, - { - self.push_contract_registration(name, RegistrationSurface::WorkerInference, contract); - self.handlers.worker_inference.insert( - name.into(), - Arc::new(move |request| Box::pin(callback(request))), - ); - } - fn register_event_sanitizer( &mut self, name: &str, @@ -685,22 +666,6 @@ impl PluginContext { surface: surface as i32, priority, break_chain, - contract: String::new(), - }); - } - - fn push_contract_registration( - &mut self, - name: &str, - surface: RegistrationSurface, - contract: &str, - ) { - self.handlers.registrations.push(Registration { - local_name: name.into(), - surface: surface as i32, - priority: 0, - break_chain: false, - contract: contract.into(), }); } } @@ -1696,12 +1661,6 @@ impl WorkerService { | RegistrationSurface::LlmExecutionIntercept => { self.invoke_llm_response(request, &scope, surface).await } - RegistrationSurface::WorkerInference => { - let payload = worker_inference_payload(request.payload)?; - let handler = self.worker_inference(&request.registration_name)?; - let future = with_thread_scope(&scope, || handler(payload)); - Ok(json_response(future.await?)) - } RegistrationSurface::LlmStreamExecutionIntercept | RegistrationSurface::Unspecified => { Err(WorkerSdkError::InvalidInput( "surface must use InvokeStream or is unspecified".into(), @@ -2097,18 +2056,6 @@ impl WorkerService { WorkerSdkError::InvalidInput(format!("llm execution '{name}' not registered")) }) } - - fn worker_inference(&self, name: &str) -> Result { - self.handlers - .lock() - .map_err(|err| WorkerSdkError::Callback(format!("handler lock poisoned: {err}")))? - .worker_inference - .get(name) - .cloned() - .ok_or_else(|| { - WorkerSdkError::InvalidInput(format!("worker inference '{name}' not registered")) - }) - } } struct ToolPayload { @@ -2225,19 +2172,6 @@ fn llm_payload( } } -fn worker_inference_payload( - payload: Option, -) -> Result { - match payload { - Some(nemo_relay_worker_proto::v1::invoke_request::Payload::WorkerInference(value)) => { - decode_json_envelope::(&value).map_err(Into::into) - } - _ => Err(WorkerSdkError::InvalidInput( - "expected worker inference payload".into(), - )), - } -} - fn required_json( value: Option, field: &str, @@ -2561,7 +2495,6 @@ fn all_surfaces() -> Vec { RegistrationSurface::MarkSanitizeGuardrail, RegistrationSurface::ScopeSanitizeStartGuardrail, RegistrationSurface::ScopeSanitizeEndGuardrail, - RegistrationSurface::WorkerInference, ] } diff --git a/crates/worker/tests/worker_sdk_tests.rs b/crates/worker/tests/worker_sdk_tests.rs index 7d95f2e49..80653155f 100644 --- a/crates/worker/tests/worker_sdk_tests.rs +++ b/crates/worker/tests/worker_sdk_tests.rs @@ -114,11 +114,6 @@ async fn worker_service_enforces_auth_and_reports_registrations() { .supported_surfaces .contains(&(RegistrationSurface::LlmStreamExecutionIntercept as i32)) ); - assert!( - handshake - .supported_surfaces - .contains(&(RegistrationSurface::WorkerInference as i32)) - ); let bad_health = client .health(Request::new(HealthRequest { @@ -205,7 +200,7 @@ async fn worker_service_enforces_auth_and_reports_registrations() { assert_eq!(invalid_register_config.code(), tonic::Code::InvalidArgument); let registrations = register_plugin(&mut client).await; - assert_eq!(registrations.len(), 22); + assert_eq!(registrations.len(), 21); for local_name in [ "llm-sanitize-request", "llm-sanitize-response", @@ -277,33 +272,6 @@ async fn worker_service_enforces_auth_and_reports_registrations() { handle.abort(); } -#[tokio::test(flavor = "multi_thread")] -async fn worker_service_invokes_worker_inference() { - let (handle, mut client) = spawn_worker( - Arc::new(SurfacePlugin::default()), - "http://127.0.0.1:9".into(), - ) - .await; - let registrations = register_plugin(&mut client).await; - assert!(registrations.iter().any(|registration| { - registration.local_name == "local-model" - && registration.surface == RegistrationSurface::WorkerInference as i32 - && registration.contract == "test.echo.v1" - })); - - let response = invoke_json( - &mut client, - worker_inference_invoke("local-model", json!({"text": "private"})), - ) - .await; - - assert_eq!( - response, - json!({"text": "private", "provider": "local-model"}) - ); - handle.abort(); -} - #[tokio::test(flavor = "multi_thread")] async fn worker_service_rejects_duplicate_registration_names_on_one_surface() { let (handle, mut client) = spawn_worker( @@ -1258,10 +1226,6 @@ async fn worker_service_reports_missing_handlers_and_malformed_payloads() { ), "llm execution", ), - ( - worker_inference_invoke("missing-worker-inference", json!({})), - "worker inference", - ), ] { assert_worker_error( client @@ -1965,9 +1929,6 @@ impl WorkerPlugin for SurfacePlugin { ctx.register_llm_stream_execution_intercept("llm-stream-open-error", 1, |_, _, _| async { Err(WorkerSdkError::Callback("stream open boom".into())) }); - ctx.register_worker_inference("local-model", "test.echo.v1", |request| async move { - Ok(set_json_field(request, "provider", "local-model")) - }); Ok(()) } } @@ -2560,21 +2521,6 @@ fn tool_invoke( } } -fn worker_inference_invoke(registration_name: &str, value: Json) -> InvokeRequest { - InvokeRequest { - activation_id: ACTIVATION_ID.into(), - invocation_id: "invoke-1".into(), - registration_name: registration_name.into(), - surface: RegistrationSurface::WorkerInference as i32, - continuation_id: String::new(), - scope: Some(scope_context()), - auth_token: AUTH_TOKEN.into(), - payload: Some( - nemo_relay_worker_proto::v1::invoke_request::Payload::WorkerInference(json_env(value)), - ), - } -} - fn llm_invoke( registration_name: &str, surface: RegistrationSurface, diff --git a/docs/about-nemo-relay/release-notes/index.mdx b/docs/about-nemo-relay/release-notes/index.mdx index 8390da450..a87eb2176 100644 --- a/docs/about-nemo-relay/release-notes/index.mdx +++ b/docs/about-nemo-relay/release-notes/index.mdx @@ -96,10 +96,8 @@ For the new callback contract and codec operations, refer to - The NeMo Guardrails remote backend inherits its configured service's availability, latency, and policy behavior. The local backend requires Python 3.11 or later and `nemoguardrails==0.22.0`. -- The PII redaction plugin supports deterministic built-in policies and - worker-backed local-model inference. Local-model mode requires a separately - installed compatible `grpc-v1` worker; the optional Rampart worker - supports Latin-script text and does not provide complete PII coverage. +- The PII redaction plugin currently supports its deterministic local backend; + local-model backend configuration is reserved for future work. - Pricing and optimization estimates depend on model names, token data, pricing sources, and freshness evidence. Missing or inconsistent evidence produces partial or absent cost fields rather than zero values. diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx index e754ed4c7..b8ca9bfbb 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx @@ -22,8 +22,8 @@ Workers implement the `PluginWorker` service: - `Handshake` and `Health` identify a ready worker. - `Validate` returns configuration diagnostics. -- `Register` returns declarative subscriber, guardrail, intercept, and worker - inference registrations. +- `Register` returns declarative subscriber, guardrail, and intercept + registrations. - `Invoke` and `InvokeStream` run registered behavior. - `CancelInvocation` requests cancellation, and `Shutdown` requests process termination. @@ -67,19 +67,12 @@ return `WorkerError` without registrations. The supported surfaces are: - `LLM_SANITIZE_REQUEST_GUARDRAIL`, `LLM_SANITIZE_RESPONSE_GUARDRAIL`, `LLM_CONDITIONAL_EXECUTION_GUARDRAIL`, `LLM_REQUEST_INTERCEPT`, `LLM_EXECUTION_INTERCEPT`, and `LLM_STREAM_EXECUTION_INTERCEPT` -- `MARK_SANITIZE_GUARDRAIL`, `SCOPE_SANITIZE_START_GUARDRAIL`, and - `SCOPE_SANITIZE_END_GUARDRAIL` -- `WORKER_INFERENCE` `InvokeRequest` identifies the registration, surface, invocation, optional continuation, and scope context. Its payload is one of an event, tool -invocation, LLM invocation, or component-owned worker inference request. -`InvokeResponse` returns an empty result, JSON result, guardrail result, LLM -request-intercept result, tool-execution result, or `WorkerError`. -`WORKER_INFERENCE` declares a versioned contract and uses a JSON request and -JSON result. The consuming first-party component selects that exact contract, -owns its schema, and resolves the callback as `/`. -`InvokeStream` emits JSON chunks or `WorkerError` chunks. +invocation, or LLM invocation. `InvokeResponse` returns an empty result, JSON +result, guardrail result, LLM request-intercept result, tool-execution result, +or `WorkerError`. `InvokeStream` emits JSON chunks or `WorkerError` chunks. Every LLM sanitizer invocation includes a directional context with tagged codec identity: `none`, `builtin(id)`, `runtime(id)`, or `opaque`. Worker SDKs expose diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx index 522326900..79d20f405 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx @@ -165,31 +165,6 @@ registers a tool request intercept that updates the request JSON and emits a mark through `PluginContext.runtime`. The `main` entrypoint blocks until Relay requests shutdown. -### Register worker inference - -Use `register_worker_inference` when a first-party Relay component owns a -versioned contract and needs an isolated detector or inference implementation: - -```python -async def detect(request: Json) -> Json: - return { - "version": 1, - "detections": await model.detect(request["texts"]), - } - - -ctx.register_worker_inference( - "detector", - "acme.pii_detection.v1", - detect, -) -``` - -Relay publishes this registration as `/detector`. The consuming -component selects the exact contract and owns its JSON schema, field selection, -deadline, response validation, and application. The worker should perform -inference only. - ## Create the Manifest Create `relay-plugin.toml` with the following content: diff --git a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx index 6e25daecf..9c5fa114f 100644 --- a/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx +++ b/docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx @@ -68,29 +68,6 @@ identity from `WorkerPlugin::plugin_id()`; custom launchers can use credentials and runs until Relay requests shutdown. Do not start the worker directly for normal operation. -### Register worker inference - -Use `register_worker_inference` when a first-party Relay component owns a -versioned contract and needs isolated inference: - -```rust -context.register_worker_inference( - "detector", - "acme.pii_detection.v1", - |request| async move { - Ok(serde_json::json!({ - "version": 1, - "detections": detect(request)? - })) - }, -); -``` - -Relay publishes this registration as `/detector`. The consuming -component selects the exact contract and owns its JSON schema, field selection, -deadline, response validation, and application. The worker should perform -inference only. - ## Package the Worker Create `relay-plugin.toml` with the following content. The artifact digest must diff --git a/docs/configure-plugins/pii-redaction/about.mdx b/docs/configure-plugins/pii-redaction/about.mdx index 3ba06c983..a37cd4812 100644 --- a/docs/configure-plugins/pii-redaction/about.mdx +++ b/docs/configure-plugins/pii-redaction/about.mdx @@ -24,8 +24,7 @@ The plugin supports these backend modes: - `builtin` - Uses a native Rust backend for deterministic payload sanitization. - `local_model` - - Delegates bounded detector inference to a manifest-backed `grpc-v1` - worker while the PII component retains sanitization policy. + - Reserves a future local-model backend lane for more stochastic detection behavior. ## Use This Plugin When @@ -40,8 +39,6 @@ Start here when you need to: first-party NeMo Relay components. - Use built-in detector presets for common values such as emails, phone numbers, URLs, API keys, and IP addresses without writing custom regexes. -- Compose deterministic recognizers with an optional local contextual - classifier without loading model dependencies into the Relay host. ## Plugin Versus Middleware @@ -93,21 +90,18 @@ The current built-in backend supports five actions: The current backend boundary is intentional: - Managed tool surfaces are sanitized as JSON payloads with exact JSON-pointer - targeting or bounded single-segment wildcard patterns. + targeting. - Managed LLM requests use the active resolved codec for each call, including built-in, runtime, and opaque codecs. Normalized response projection requires a recognized built-in codec because response codec capabilities are decode-only. This lets redaction target normalized Relay shapes such as - `/messages/*/content` and `/message`. + `/messages/0/content` and `/message`. - `mark` sanitizes `data`, `category_profile`, and `metadata` independently on every mark event. It defaults to `true`; set `mark = false` to opt out. - `input`, `output`, `tool_input`, and `tool_output` sanitize scope metadata on their corresponding lifecycle events. Tool and LLM primary data and typed profiles remain on their specialized sanitizer paths to avoid applying the same mask or hash twice. -- Local-model workers receive only the selected string values. Relay owns - batching, deadlines, confidence and label policy, response validation, - replacement, and fail-closed behavior. ## Observability Boundary @@ -122,25 +116,20 @@ That means: For managed LLM requests, codec decode and re-encode can canonicalize the emitted provider-shaped start event. For example, Relay can record an OpenAI Responses request in the codec's canonical `input` array form rather than the -original shorthand form. If codec processing fails, the local-model backend -omits the emitted LLM body rather than applying normalized selectors to an -incompatible raw provider shape. +original shorthand form. ## Current Boundaries This plugin is intentionally scoped to a deterministic built-in backend plus a -local worker inference extension point. +future local-model extension point. In particular: -- Local-model workers are optional dynamic plugins. They register a - language-neutral JSON request-response contract through the `grpc-v1` - worker protocol. +- `local_model` is an extension point, not a complete backend implementation + today. - The plugin does not mutate the real callback arguments or return values. -- `target_path_patterns` supports `*` as one complete JSON-pointer path - segment. It does not support recursive or partial-segment matching. -- `allow_network = false` prohibits network inference by contract, but the - worker process is not a network sandbox. Install only trusted workers. +- The plugin does not add a subtree or prefix selector language beyond exact + JSON-pointer matching. ## Pages diff --git a/docs/configure-plugins/pii-redaction/configuration.mdx b/docs/configure-plugins/pii-redaction/configuration.mdx index 2a1688960..d10f84813 100644 --- a/docs/configure-plugins/pii-redaction/configuration.mdx +++ b/docs/configure-plugins/pii-redaction/configuration.mdx @@ -128,14 +128,14 @@ The following table compares the available PII redaction backends: | Area | `builtin` | `local_model` | | --- | --- | --- | | Built-in component kind and config validation | Supported | Supported | -| Managed LLM `input` | Supported | Supported | -| Managed LLM `output` | Supported | Supported | -| Mark and generic scope event fields | Supported | Supported | -| Managed `tool_input` | Supported | Supported | -| Managed `tool_output` | Supported | Supported | +| Managed LLM `input` | Supported | Not implemented | +| Managed LLM `output` | Supported | Not implemented | +| Mark and generic scope event fields | Supported | Not implemented | +| Managed `tool_input` | Supported | Not implemented | +| Managed `tool_output` | Supported | Not implemented | | Built-in actions | `remove`, `redact`, `regex_replace`, `hash`, `mask` | N/A | -| Codec support | `openai_chat`, `openai_responses`, `anthropic_messages` | `openai_chat`, `openai_responses`, `anthropic_messages` | -| Runtime availability | Any runtime that includes the `nemo-relay-pii-redaction` plugin crate | Runtimes with active `grpc-v1` worker inference | +| Codec support | `openai_chat`, `openai_responses`, `anthropic_messages` | Runtime-specific future implementation | +| Runtime availability | Any runtime that includes the `nemo-relay-pii-redaction` plugin crate | Runtimes that install a local backend provider | ## Built-in Mode @@ -217,8 +217,7 @@ Use the editor when you want to: - Set the LLM `codec` - Edit `builtin` action settings such as `action`, `target_paths`, `pattern`, `detector`, `replacement`, and masking fields -- Edit worker-backed local-model settings such as `local.backend`, - `target_paths`, `target_path_patterns`, `min_score`, and `excluded_labels` +- Edit `local.backend` for a runtime-provided future local-model backend The editor preserves unknown fields when it rewrites an existing `pii_redaction` component, so future or runtime-specific settings are not @@ -315,120 +314,9 @@ When `detector` is set and you do not specify `unmasked_prefix` or - `gcp_api_key`: Preserves the `AIza`-style prefix and the last four characters - `azure_storage_account_key`: Preserves the last four characters -## Local-Model Mode - -Use `local_model` when a manifest-backed `grpc-v1` worker should detect -contextual PII. The worker performs inference only. The PII component selects -fields, batches text, enforces deadlines, validates detections, applies -confidence and label policy, replaces accepted spans, and fails closed. - -The worker inference name is `/`. For example, a worker -with plugin ID `acme.pii_worker` that registers `detector` for -`nemo.relay.pii_detection.v1` is selected as `acme.pii_worker/detector`. Relay -rejects registrations that declare another contract before installing the -sanitizer. - -```toml -[[components]] -kind = "pii_redaction" -enabled = true - -[components.config] -mode = "local_model" -codec = "openai_chat" - -[components.config.local] -backend = "acme.pii_worker/detector" -model_id = "acme-pii-v1" -detector_profile = "default" -min_score = 0.4 -excluded_labels = ["ORGANIZATION"] -target_path_patterns = [ - "/messages/*/content", - "/messages/*/content/*/text", - "/message", - "/message/*/text", -] -replacement = "[REDACTED]" -allow_network = false -max_latency_ms = 250 -``` - -Worker inference is installed before built-in components initialize and is -removed after their dependent sanitizers. `allow_network = true` is rejected: -this lane is for same-machine inference. This is a configuration contract, not -a process sandbox. - -Worker failures, timeouts, malformed responses, invalid UTF-8 spans, -overlapping spans, and input-limit violations fail closed for the affected -batch. If a configured codec cannot decode or safely re-encode an LLM payload, -Relay omits that request or response payload from the emitted event. The -default deadline is 250 ms for the complete selected payload, including every -inference batch. Configuration above 60 seconds is rejected. - -Use `profiles` to run deterministic recognizers before a contextual model: - -```toml -[components.config] -codec = "openai_chat" - -[[components.config.profiles]] -mode = "builtin" -priority = 80 - -[components.config.profiles.builtin] -action = "redact" -detector = "email" - -[[components.config.profiles]] -mode = "local_model" -priority = 90 - -[components.config.profiles.local] -backend = "nemo_relay.pii_rampart/detector" -min_score = 0.4 -max_latency_ms = 5000 -target_path_patterns = [ - "/messages/*/content", - "/messages/*/content/*/text", - "/message", - "/message/*/text", -] -``` - -Keep contextual classifiers limited to normalized content paths. Sending model -names, tool IDs, trace IDs, routing fields, or arbitrary provider metadata to -a classifier increases false positives and exposes data outside the intended -policy boundary. - -The generic local-model payload deadline defaults to 250 ms. Contextual models -can need more time for large selected payloads. The Rampart profile above uses -5000 ms so a request near the 64 KiB detection-request limit has practical headroom on -typical client hardware; benchmark representative inputs on deployment -hardware before lowering it. - -The optional Rampart worker is included as a manifest-backed Python source -bundle under `crates/pii-redaction/workers/rampart`. Prefetch its pinned model -before adding and enabling it from a source checkout: - -```bash -cd crates/pii-redaction/workers/rampart -uvx --from . nemo-relay-pii-rampart-prefetch -nemo-relay plugins add ./relay-plugin.toml -nemo-relay plugins enable nemo_relay.pii_rampart -``` - -Activation remains offline-only. The prefetch command and activation both -verify the pinned model files before ONNX Runtime loads them. - ## Path Semantics -`target_paths` are exact JSON-pointer matches. `target_path_patterns` also -accepts `*` as one complete path segment. It does not provide recursive or -partial-segment matching. When both lists are empty, the selected backend -inspects every string leaf and reports a configuration warning. Use explicit -content paths for contextual local models so identifiers and provider metadata -do not enter the classifier accidentally. +`target_paths` are exact JSON-pointer matches. The plugin uses different payload boundaries for tools and LLMs: @@ -438,8 +326,7 @@ The plugin uses different payload boundaries for tools and LLMs: responses use the active built-in codec for normalized projection. Prefer normalized Relay paths such as: - `/headers/authorization` for an exact request header field - - `/messages/*/content` for request message content - - `/messages/*/content/*/text` for multimodal request text + - `/messages/0/content` for request message content - `/message` for the normalized assistant response text - Marks and non-tool, non-LLM scopes sanitize `data`, `category_profile`, and `metadata` as separate JSON values. A target path is evaluated independently @@ -519,3 +406,23 @@ For tool and LLM scope events, the PII redaction scope sanitizer leaves `data`, LLM sanitizer already owns that lifecycle boundary. This prevents repeated hashing or masking. Other scope categories use `input` for starts and `output` for ends. + +## Local Model Mode + +`local_model` is reserved for a future in-process local-model backend. + +### Current Status + +The current local-model status is: + +- The plugin contract accepts `mode = "local_model"`. +- The `local` section supports: + - `backend` + - `model_id` + - `detector_profile` + - `allow_network` + - `max_latency_ms` +- Actual behavior depends on a runtime-installed local backend provider. + +Without a provider, runtimes report the local backend as unavailable during +plugin initialization. diff --git a/go/nemo_relay/pii_redaction.go b/go/nemo_relay/pii_redaction.go index 43693abec..871a83b60 100644 --- a/go/nemo_relay/pii_redaction.go +++ b/go/nemo_relay/pii_redaction.go @@ -3,8 +3,6 @@ package nemo_relay -import "encoding/json" - // PiiRedactionPluginKind is the top-level plugin kind used by the built-in PII redaction component. const PiiRedactionPluginKind = "pii_redaction" @@ -20,27 +18,13 @@ type PiiRedactionBuiltinConfig struct { UnmaskedSuffix *int32 `json:"unmasked_suffix,omitempty"` } -// PiiRedactionLocalModelConfig configures a worker-backed local-model redaction provider. +// PiiRedactionLocalModelConfig configures the future local-model redaction backend. type PiiRedactionLocalModelConfig struct { - Backend string `json:"backend,omitempty"` - ModelID string `json:"model_id,omitempty"` - DetectorProfile string `json:"detector_profile,omitempty"` - TargetPaths []string `json:"target_paths,omitempty"` - TargetPathPatterns []string `json:"target_path_patterns,omitempty"` - MinScore *float64 `json:"min_score,omitempty"` - ExcludedLabels []string `json:"excluded_labels,omitempty"` - Replacement *string `json:"replacement,omitempty"` - AllowNetwork *bool `json:"allow_network,omitempty"` - MaxLatencyMS *int32 `json:"max_latency_ms,omitempty"` -} - -// PiiRedactionProfile configures one ordered PII redaction backend. -type PiiRedactionProfile struct { - Enabled bool `json:"enabled"` - Mode string `json:"mode"` - Priority int32 `json:"priority"` - Builtin *PiiRedactionBuiltinConfig `json:"builtin,omitempty"` - Local *PiiRedactionLocalModelConfig `json:"local,omitempty"` + Backend string `json:"backend,omitempty"` + ModelID string `json:"model_id,omitempty"` + DetectorProfile string `json:"detector_profile,omitempty"` + AllowNetwork *bool `json:"allow_network,omitempty"` + MaxLatencyMS *int32 `json:"max_latency_ms,omitempty"` } // PiiRedactionConfig is the canonical Go shape for the PII redaction plugin config document. @@ -54,31 +38,11 @@ type PiiRedactionConfig struct { ToolOutput bool `json:"tool_output"` Priority int32 `json:"priority,omitempty"` Codec string `json:"codec,omitempty"` - Profiles []PiiRedactionProfile `json:"profiles,omitempty"` Builtin *PiiRedactionBuiltinConfig `json:"builtin,omitempty"` Local *PiiRedactionLocalModelConfig `json:"local,omitempty"` Policy *ConfigPolicy `json:"policy,omitempty"` } -// MarshalJSON omits legacy top-level fields when profile composition is used. -func (config PiiRedactionConfig) MarshalJSON() ([]byte, error) { - if len(config.Profiles) == 0 { - type configAlias PiiRedactionConfig - return json.Marshal(configAlias(config)) - } - return json.Marshal(struct { - Version uint32 `json:"version,omitempty"` - Codec string `json:"codec,omitempty"` - Profiles []PiiRedactionProfile `json:"profiles"` - Policy *ConfigPolicy `json:"policy,omitempty"` - }{ - Version: config.Version, - Codec: config.Codec, - Profiles: config.Profiles, - Policy: config.Policy, - }) -} - // PiiRedactionComponentSpec wraps one PII redaction config as a top-level plugin component. type PiiRedactionComponentSpec struct { Enabled bool `json:"enabled,omitempty"` @@ -114,15 +78,6 @@ func NewPiiRedactionLocalModelConfig() PiiRedactionLocalModelConfig { return PiiRedactionLocalModelConfig{} } -// NewPiiRedactionProfile returns one enabled built-in profile with default priority. -func NewPiiRedactionProfile() PiiRedactionProfile { - return PiiRedactionProfile{ - Enabled: true, - Mode: "builtin", - Priority: 100, - } -} - // NewPiiRedactionComponentSpec wraps PII redaction config as an enabled component. func NewPiiRedactionComponentSpec(config PiiRedactionConfig) PiiRedactionComponentSpec { return PiiRedactionComponentSpec{ diff --git a/go/nemo_relay/pii_redaction/pii_redaction.go b/go/nemo_relay/pii_redaction/pii_redaction.go index 68297995a..5a127ed96 100644 --- a/go/nemo_relay/pii_redaction/pii_redaction.go +++ b/go/nemo_relay/pii_redaction/pii_redaction.go @@ -11,12 +11,9 @@ type Config = nemo_relay.PiiRedactionConfig // BuiltinConfig configures deterministic built-in redaction. type BuiltinConfig = nemo_relay.PiiRedactionBuiltinConfig -// LocalModelConfig configures a worker-backed local-model redaction provider. +// LocalModelConfig configures the future local-model redaction backend. type LocalModelConfig = nemo_relay.PiiRedactionLocalModelConfig -// Profile configures one ordered PII redaction backend. -type Profile = nemo_relay.PiiRedactionProfile - // ComponentSpec wraps PII redaction config as a top-level plugin component. type ComponentSpec = nemo_relay.PiiRedactionComponentSpec @@ -44,11 +41,6 @@ func NewLocalModelConfig() LocalModelConfig { return nemo_relay.NewPiiRedactionLocalModelConfig() } -// NewProfile returns one enabled built-in profile with default priority. -func NewProfile() Profile { - return nemo_relay.NewPiiRedactionProfile() -} - // NewComponentSpec wraps PII redaction config as an enabled component. func NewComponentSpec(config Config) ComponentSpec { return nemo_relay.NewPiiRedactionComponentSpec(config) diff --git a/go/nemo_relay/pii_redaction/pii_redaction_test.go b/go/nemo_relay/pii_redaction/pii_redaction_test.go index 95c62a127..3bb3301a2 100644 --- a/go/nemo_relay/pii_redaction/pii_redaction_test.go +++ b/go/nemo_relay/pii_redaction/pii_redaction_test.go @@ -27,39 +27,13 @@ func TestPiiRedactionShorthandHelpers(t *testing.T) { func TestPiiRedactionComponentSpecAndLocalModelHelpers(t *testing.T) { config := NewConfig() local := NewLocalModelConfig() - minScore := 0.75 - replacement := "[PRIVATE]" - allowNetwork := false - maxLatencyMS := int32(250) - local.Backend = "nemo_relay.pii_rampart/detector" + local.Backend = "local" local.ModelID = "pii-model" - local.DetectorProfile = "default" - local.TargetPaths = []string{"/message"} - local.TargetPathPatterns = []string{"/messages/*/content"} - local.MinScore = &minScore - local.ExcludedLabels = []string{"CITY"} - local.Replacement = &replacement - local.AllowNetwork = &allowNetwork - local.MaxLatencyMS = &maxLatencyMS - config.Mode = "local_model" + config.Mode = "local" config.Local = &local spec := NewComponentSpec(config) - if !spec.Enabled || - spec.Config.Local == nil || - spec.Config.Local.ModelID != "pii-model" || - spec.Config.Local.DetectorProfile != "default" || - len(spec.Config.Local.TargetPaths) != 1 || - len(spec.Config.Local.TargetPathPatterns) != 1 || - spec.Config.Local.MinScore == nil || - *spec.Config.Local.MinScore != minScore || - len(spec.Config.Local.ExcludedLabels) != 1 || - spec.Config.Local.Replacement == nil || - *spec.Config.Local.Replacement != replacement || - spec.Config.Local.AllowNetwork == nil || - *spec.Config.Local.AllowNetwork || - spec.Config.Local.MaxLatencyMS == nil || - *spec.Config.Local.MaxLatencyMS != maxLatencyMS { + if !spec.Enabled || spec.Config.Local == nil || spec.Config.Local.ModelID != "pii-model" { t.Fatalf("unexpected PII redaction component spec: %#v", spec) } } diff --git a/go/nemo_relay/pii_redaction_test.go b/go/nemo_relay/pii_redaction_test.go index d9591fbed..5b5728adc 100644 --- a/go/nemo_relay/pii_redaction_test.go +++ b/go/nemo_relay/pii_redaction_test.go @@ -3,11 +3,7 @@ package nemo_relay -import ( - "encoding/json" - "reflect" - "testing" -) +import "testing" func TestPiiRedactionConfigHelpers(t *testing.T) { config := NewPiiRedactionConfig() @@ -22,13 +18,9 @@ func TestPiiRedactionConfigHelpers(t *testing.T) { t.Fatalf("unexpected built-in redaction defaults: %#v", builtin) } local := NewPiiRedactionLocalModelConfig() - if !reflect.DeepEqual(local, PiiRedactionLocalModelConfig{}) { + if local != (PiiRedactionLocalModelConfig{}) { t.Fatalf("unexpected local model defaults: %#v", local) } - profile := NewPiiRedactionProfile() - if !profile.Enabled || profile.Mode != "builtin" || profile.Priority != 100 { - t.Fatalf("unexpected profile defaults: %#v", profile) - } config.Builtin = &builtin component := PiiRedactionComponent(config) @@ -50,30 +42,6 @@ func TestPiiRedactionConfigHelpers(t *testing.T) { } } -func TestPiiRedactionProfilesOmitLegacyTopLevelFields(t *testing.T) { - config := NewPiiRedactionConfig() - config.Profiles = []PiiRedactionProfile{ - NewPiiRedactionProfile(), - } - serialized, err := json.Marshal(config) - if err != nil { - t.Fatalf("marshal profile config: %v", err) - } - var value map[string]any - if err := json.Unmarshal(serialized, &value); err != nil { - t.Fatalf("decode profile config: %v", err) - } - if _, present := value["mode"]; present { - t.Fatalf("profile config retained legacy mode: %#v", value) - } - if _, present := value["input"]; present { - t.Fatalf("profile config retained legacy input: %#v", value) - } - if len(value["profiles"].([]any)) != 1 { - t.Fatalf("unexpected profile config: %#v", value) - } -} - func TestPiiRedactionValidationRejectsBadValues(t *testing.T) { config := NewPiiRedactionConfig() config.Input = false diff --git a/justfile b/justfile index bbbe2513e..c64a671ab 100644 --- a/justfile +++ b/justfile @@ -971,9 +971,6 @@ check-python-worker-proto: } assert pb.SUBSCRIBER == 1 assert pb.LLM_STREAM_EXECUTION_INTERCEPT == 25 - assert pb.WORKER_INFERENCE == 40 - assert pb.Registration.DESCRIPTOR.fields_by_name["contract"].number == 5 - assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["worker_inference"].number == 13 PY generate-worker-plugin-lockfile: diff --git a/python/nemo_relay/pii_redaction.py b/python/nemo_relay/pii_redaction.py index de4f443d0..53c2b77f8 100644 --- a/python/nemo_relay/pii_redaction.py +++ b/python/nemo_relay/pii_redaction.py @@ -103,16 +103,11 @@ def to_dict(self) -> JsonObject: @dataclass(slots=True) class LocalModelConfig: - """Worker-backed local-model redaction settings.""" + """Future local-model backend seam settings.""" backend: str | None = None model_id: str | None = None detector_profile: str | None = None - target_paths: list[str] = field(default_factory=list) - target_path_patterns: list[str] = field(default_factory=list) - min_score: float | None = None - excluded_labels: list[str] = field(default_factory=list) - replacement: str | None = None allow_network: bool | None = None max_latency_ms: int | None = None @@ -123,40 +118,12 @@ def to_dict(self) -> JsonObject: "backend": self.backend, "model_id": self.model_id, "detector_profile": self.detector_profile, - "target_paths": self.target_paths or None, - "target_path_patterns": self.target_path_patterns or None, - "min_score": self.min_score, - "excluded_labels": self.excluded_labels or None, - "replacement": self.replacement, "allow_network": self.allow_network, "max_latency_ms": self.max_latency_ms, } ) -@dataclass(slots=True) -class PiiRedactionProfile: - """One ordered PII redaction backend profile.""" - - enabled: bool = True - mode: Literal["builtin", "local_model"] = "builtin" - priority: int = 100 - builtin: BuiltinConfig | None = None - local: LocalModelConfig | None = None - - def to_dict(self) -> JsonObject: - """Serialize this profile to the canonical JSON object shape.""" - return _normalize_object( - { - "enabled": self.enabled, - "mode": self.mode, - "priority": self.priority, - "builtin": self.builtin, - "local": self.local, - } - ) - - @dataclass(slots=True) class PiiRedactionConfig: """Canonical config document for the top-level PII redaction component.""" @@ -170,22 +137,12 @@ class PiiRedactionConfig: mark: bool = True priority: int = 100 codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = None - profiles: list[PiiRedactionProfile] = field(default_factory=list) builtin: BuiltinConfig | None = None local: LocalModelConfig | None = None policy: ConfigPolicy = field(default_factory=ConfigPolicy) def to_dict(self) -> JsonObject: """Serialize this PII redaction config to the canonical JSON object shape.""" - if self.profiles: - return _normalize_object( - { - "version": self.version, - "codec": self.codec, - "profiles": self.profiles, - "policy": self.policy, - } - ) return _normalize_object( { "version": self.version, @@ -230,7 +187,7 @@ def validate_config(config: PiiRedactionConfig | JsonObject) -> ConfigReport: components=[ComponentSpec(config)], ) ) - return report + return cast(ConfigReport, report) __all__ = [ @@ -242,6 +199,5 @@ def validate_config(config: PiiRedactionConfig | JsonObject) -> ConfigReport: "LocalModelConfig", "PII_REDACTION_PLUGIN_KIND", "PiiRedactionConfig", - "PiiRedactionProfile", "validate_config", ] diff --git a/python/nemo_relay/pii_redaction.pyi b/python/nemo_relay/pii_redaction.pyi index fd6abaa72..244f6a3ef 100644 --- a/python/nemo_relay/pii_redaction.pyi +++ b/python/nemo_relay/pii_redaction.pyi @@ -44,24 +44,10 @@ class LocalModelConfig: backend: str | None = ... model_id: str | None = ... detector_profile: str | None = ... - target_paths: list[str] = field(default_factory=list) - target_path_patterns: list[str] = field(default_factory=list) - min_score: float | None = ... - excluded_labels: list[str] = field(default_factory=list) - replacement: str | None = ... allow_network: bool | None = ... max_latency_ms: int | None = ... def to_dict(self) -> JsonObject: ... -@dataclass(slots=True) -class PiiRedactionProfile: - enabled: bool = ... - mode: Literal["builtin", "local_model"] = ... - priority: int = ... - builtin: BuiltinConfig | None = ... - local: LocalModelConfig | None = ... - def to_dict(self) -> JsonObject: ... - @dataclass(slots=True) class PiiRedactionConfig: version: int = ... @@ -73,7 +59,6 @@ class PiiRedactionConfig: mark: bool = ... priority: int = ... codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = ... - profiles: list[PiiRedactionProfile] = field(default_factory=list) builtin: BuiltinConfig | None = ... local: LocalModelConfig | None = ... policy: ConfigPolicy = field(default_factory=ConfigPolicy) diff --git a/python/plugin/README.md b/python/plugin/README.md index 3741e6fa1..051383f8d 100644 --- a/python/plugin/README.md +++ b/python/plugin/README.md @@ -26,8 +26,7 @@ protocol. - **Isolate plugin dependencies**: Run custom policy, middleware, or exporter code outside the Relay host process. - **Use the shared runtime contract**: Register subscribers, guardrails, and - intercepts or worker inference callbacks through `WorkerPlugin` and - `PluginContext`. + intercepts through `WorkerPlugin` and `PluginContext`. - **Call back into Relay safely**: Emit marks, create scopes, and continue managed execution through the host runtime handle. - **Keep worker lifecycle managed**: Let Relay provision the worker environment, @@ -105,31 +104,6 @@ worker process. For a complete manifest and runnable plugin, see the [Python gRPC worker plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/python-grpc-worker-plugin/README.md). -## Worker Inference - -Use `register_worker_inference` when a first-party Relay component owns a -versioned request-response contract and needs isolated model inference: - -```python -async def detect(request: Json) -> Json: - return { - "version": 1, - "detections": await model.detect(request["texts"]), - } - - -ctx.register_worker_inference( - "detector", - "acme.pii_detection.v1", - detect, -) -``` - -Relay publishes this callback as `/detector`. The callback may be -synchronous or asynchronous and should perform inference only. The consuming -host component selects the exact contract and owns the payload schema, -deadline, field traversal, output validation, and result application. - ## Request Intercepts LLM request intercepts return one canonical outcome: diff --git a/python/plugin/src/nemo_relay_plugin/__init__.py b/python/plugin/src/nemo_relay_plugin/__init__.py index cd1bd68eb..54466a5b5 100644 --- a/python/plugin/src/nemo_relay_plugin/__init__.py +++ b/python/plugin/src/nemo_relay_plugin/__init__.py @@ -36,7 +36,6 @@ LlmOptimizationTokens: Explicit token evidence by category. LlmOptimizationTokenImpact: Baseline, effective, and saved token evidence. LlmRequestInterceptOutcome: Canonical LLM request-intercept result. - WorkerInferenceCallback: Versioned worker inference callback. ToolExecutionInterceptOutcome: Canonical tool execution-intercept result. DiagnosticLevel: Severity of a configuration diagnostic. ConfigDiagnostic: Structured configuration warning or error. @@ -108,7 +107,6 @@ ToolNext, ToolRequestCallback, ToolSanitizeCallback, - WorkerInferenceCallback, WorkerPlugin, WorkerRequestCodec, WorkerResponseCodec, @@ -144,7 +142,6 @@ "LlmSanitizeResponseCallback", "LlmStreamNext", "LlmStreamExecutionCallback", - "WorkerInferenceCallback", "PluginContext", "PluginRuntime", "PendingMarkSpec", diff --git a/python/plugin/src/nemo_relay_plugin/_api.py b/python/plugin/src/nemo_relay_plugin/_api.py index cc32fb441..7f8b6ccb8 100644 --- a/python/plugin/src/nemo_relay_plugin/_api.py +++ b/python/plugin/src/nemo_relay_plugin/_api.py @@ -845,7 +845,6 @@ def register(self, ctx: PluginContext, config: Json) -> None | Awaitable[None]: [str, LlmRequest, "LlmStreamNext"], Iterable[Json] | AsyncIterator[Json] | Awaitable[Iterable[Json] | AsyncIterator[Json]], ] -WorkerInferenceCallback: TypeAlias = Callable[[Json], Json | Awaitable[Json]] @dataclass(slots=True) @@ -866,7 +865,6 @@ class _Handlers: llm_requests: dict[str, LlmRequestCallback] llm_executions: dict[str, LlmExecutionCallback] llm_stream_executions: dict[str, LlmStreamExecutionCallback] - worker_inference: dict[str, WorkerInferenceCallback] @classmethod def empty(cls) -> _Handlers: @@ -887,7 +885,6 @@ def empty(cls) -> _Handlers: llm_requests={}, llm_executions={}, llm_stream_executions={}, - worker_inference={}, ) @@ -956,28 +953,6 @@ def register_subscriber(self, name: str, callback: SubscriberCallback) -> None: self._push_registration(name, pb.SUBSCRIBER, 0, False) self._handlers.subscribers[name] = callback - def register_worker_inference( - self, - name: str, - contract: str, - callback: WorkerInferenceCallback, - ) -> None: - """Register named worker inference for a versioned host contract. - - Args: - name: Stable inference name selected by a consuming host component. - contract: Versioned request-response contract implemented by the worker. - callback: Function receiving and returning component-owned JSON. - The callback can return a value directly or through an - awaitable. - - Ownership boundary: - Workers perform model inference only. The consuming host - component owns field selection, policy, and output application. - """ - self._push_registration(name, pb.WORKER_INFERENCE, 0, False, contract=contract) - self._handlers.worker_inference[name] = callback - def _register_event_sanitizer( self, name: str, @@ -1285,15 +1260,7 @@ def register_llm_stream_execution_intercept( self._push_registration(name, pb.LLM_STREAM_EXECUTION_INTERCEPT, priority, False) self._handlers.llm_stream_executions[name] = callback - def _push_registration( - self, - name: str, - surface: int, - priority: int, - break_chain: bool, - *, - contract: str = "", - ) -> None: + def _push_registration(self, name: str, surface: int, priority: int, break_chain: bool) -> None: if any( registration.local_name == name and registration.surface == surface for registration in self._handlers.registrations @@ -1305,7 +1272,6 @@ def _push_registration( surface=surface, priority=priority, break_chain=break_chain, - contract=contract, ) ) @@ -2094,19 +2060,6 @@ async def _invoke_result(self, request: Any) -> Any: ), ) ) - if request.surface == pb.WORKER_INFERENCE: - result = await _maybe_await( - self._handler( - self._handlers.worker_inference, - request.registration_name, - )( - _decode_required_envelope( - request.worker_inference, - "worker inference request", - ) - ) - ) - return _json_response(result) raise WorkerSdkError(f"unsupported registration surface {request.surface}") async def _invoke_llm_result(self, request: Any) -> Any: @@ -2242,7 +2195,6 @@ def _all_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, - pb.WORKER_INFERENCE, ] diff --git a/python/tests/plugin/test_public_api_docstrings.py b/python/tests/plugin/test_public_api_docstrings.py index 7b77ccbde..31abf7427 100644 --- a/python/tests/plugin/test_public_api_docstrings.py +++ b/python/tests/plugin/test_public_api_docstrings.py @@ -35,7 +35,6 @@ "LlmRequestCallback", "LlmExecutionCallback", "LlmStreamExecutionCallback", - "WorkerInferenceCallback", } diff --git a/python/tests/plugin/test_worker_sdk.py b/python/tests/plugin/test_worker_sdk.py index 09be852a3..7ab918d20 100644 --- a/python/tests/plugin/test_worker_sdk.py +++ b/python/tests/plugin/test_worker_sdk.py @@ -363,9 +363,6 @@ async def llm_stream_execution(name: str, request: Json, next_call: Any) -> Asyn async for chunk in stream: yield _tag(chunk, "llm_stream_execution") - async def worker_inference(request: Json) -> Json: - return _tag(request, "local_model") - ctx.register_subscriber("subscriber", subscriber) ctx.register_mark_sanitize_guardrail("event_sanitize", mark_sanitize, priority=1) ctx.register_scope_sanitize_start_guardrail("event_sanitize", scope_start_sanitize, priority=2) @@ -381,11 +378,6 @@ async def worker_inference(request: Json) -> Json: ctx.register_llm_request_intercept("llm_request", llm_request, priority=9, break_chain=True) ctx.register_llm_execution_intercept("llm_execution", llm_execution, priority=10) ctx.register_llm_stream_execution_intercept("llm_stream_execution", llm_stream_execution, priority=11) - ctx.register_worker_inference( - "local_model", - "test.echo.v1", - worker_inference, - ) @pytest.fixture(name="host_stub") @@ -419,9 +411,6 @@ def test_generated_proto_matches_worker_contract(): assert pb.MARK_SANITIZE_GUARDRAIL == 30 assert pb.SCOPE_SANITIZE_START_GUARDRAIL == 31 assert pb.SCOPE_SANITIZE_END_GUARDRAIL == 32 - assert pb.WORKER_INFERENCE == 40 - assert pb.Registration.DESCRIPTOR.fields_by_name["contract"].number == 5 - assert pb.InvokeRequest.DESCRIPTOR.fields_by_name["worker_inference"].number == 13 assert pb.CUSTOM == 10 @@ -455,32 +444,25 @@ async def test_health_handshake_validate_register_and_all_surfaces(service: _Wor register = await _register(service) registrations = [ - ( - registration.local_name, - registration.surface, - registration.priority, - registration.break_chain, - registration.contract, - ) + (registration.local_name, registration.surface, registration.priority, registration.break_chain) for registration in register.registrations ] assert registrations == [ - ("subscriber", pb.SUBSCRIBER, 0, False, ""), - ("event_sanitize", pb.MARK_SANITIZE_GUARDRAIL, 1, False, ""), - ("event_sanitize", pb.SCOPE_SANITIZE_START_GUARDRAIL, 2, False, ""), - ("scope_end_sanitize", pb.SCOPE_SANITIZE_END_GUARDRAIL, 3, False, ""), - ("tool_sanitize", pb.TOOL_SANITIZE_REQUEST_GUARDRAIL, 1, False, ""), - ("tool_sanitize", pb.TOOL_SANITIZE_RESPONSE_GUARDRAIL, 2, False, ""), - ("tool_conditional", pb.TOOL_CONDITIONAL_EXECUTION_GUARDRAIL, 3, False, ""), - ("tool_request", pb.TOOL_REQUEST_INTERCEPT, 4, True, ""), - ("tool_execution", pb.TOOL_EXECUTION_INTERCEPT, 5, False, ""), - ("llm_sanitize_request", pb.LLM_SANITIZE_REQUEST_GUARDRAIL, 6, False, ""), - ("llm_sanitize_response", pb.LLM_SANITIZE_RESPONSE_GUARDRAIL, 7, False, ""), - ("llm_conditional", pb.LLM_CONDITIONAL_EXECUTION_GUARDRAIL, 8, False, ""), - ("llm_request", pb.LLM_REQUEST_INTERCEPT, 9, True, ""), - ("llm_execution", pb.LLM_EXECUTION_INTERCEPT, 10, False, ""), - ("llm_stream_execution", pb.LLM_STREAM_EXECUTION_INTERCEPT, 11, False, ""), - ("local_model", pb.WORKER_INFERENCE, 0, False, "test.echo.v1"), + ("subscriber", pb.SUBSCRIBER, 0, False), + ("event_sanitize", pb.MARK_SANITIZE_GUARDRAIL, 1, False), + ("event_sanitize", pb.SCOPE_SANITIZE_START_GUARDRAIL, 2, False), + ("scope_end_sanitize", pb.SCOPE_SANITIZE_END_GUARDRAIL, 3, False), + ("tool_sanitize", pb.TOOL_SANITIZE_REQUEST_GUARDRAIL, 1, False), + ("tool_sanitize", pb.TOOL_SANITIZE_RESPONSE_GUARDRAIL, 2, False), + ("tool_conditional", pb.TOOL_CONDITIONAL_EXECUTION_GUARDRAIL, 3, False), + ("tool_request", pb.TOOL_REQUEST_INTERCEPT, 4, True), + ("tool_execution", pb.TOOL_EXECUTION_INTERCEPT, 5, False), + ("llm_sanitize_request", pb.LLM_SANITIZE_REQUEST_GUARDRAIL, 6, False), + ("llm_sanitize_response", pb.LLM_SANITIZE_RESPONSE_GUARDRAIL, 7, False), + ("llm_conditional", pb.LLM_CONDITIONAL_EXECUTION_GUARDRAIL, 8, False), + ("llm_request", pb.LLM_REQUEST_INTERCEPT, 9, True), + ("llm_execution", pb.LLM_EXECUTION_INTERCEPT, 10, False), + ("llm_stream_execution", pb.LLM_STREAM_EXECUTION_INTERCEPT, 11, False), ] @@ -1456,16 +1438,6 @@ async def test_unary_invoke_success_paths(service: _WorkerService, host_stub: Re assert llm_execution["tag"] == "llm_execution" assert llm_execution["next_llm"]["content"]["llm_execute_gpt-test"] - local_model = await service.Invoke( - _worker_inference_request("local_model", {"text": "private"}), - AbortContext(), - ) - assert local_model.WhichOneof("result") == "json" - assert _envelope_value(local_model.json.value) == { - "text": "private", - "tag": "local_model", - } - async def test_unary_invoke_failure_paths(service: _WorkerService): await _register(service) @@ -1480,13 +1452,6 @@ async def test_unary_invoke_failure_paths(service: _WorkerService): assert missing_handler.WhichOneof("result") == "error" assert "not registered" in missing_handler.error.message - missing_provider = await service.Invoke( - _worker_inference_request("missing", {}), - AbortContext(), - ) - assert missing_provider.WhichOneof("result") == "error" - assert "not registered" in missing_provider.error.message - unsupported = await service.Invoke( _tool_request("tool_request", pb.REGISTRATION_SURFACE_UNSPECIFIED, {}), AbortContext(), @@ -2773,15 +2738,6 @@ def _tool_request(registration_name: str, surface: int, value: Json) -> Any: ) -def _worker_inference_request(registration_name: str, value: Json) -> Any: - return _invoke_request( - registration_name, - pb.WORKER_INFERENCE, - continuation_id="", - worker_inference=_json_envelope(JSON_SCHEMA, value), - ) - - def _llm_payload( *, model_name: str = "model", @@ -2870,5 +2826,4 @@ def _all_expected_surfaces() -> list[int]: pb.LLM_REQUEST_INTERCEPT, pb.LLM_EXECUTION_INTERCEPT, pb.LLM_STREAM_EXECUTION_INTERCEPT, - pb.WORKER_INFERENCE, ] diff --git a/python/tests/test_pii_redaction_plugin.py b/python/tests/test_pii_redaction_plugin.py index ab57329b3..cbed8c4c2 100644 --- a/python/tests/test_pii_redaction_plugin.py +++ b/python/tests/test_pii_redaction_plugin.py @@ -13,7 +13,6 @@ ConfigPolicy, LocalModelConfig, PiiRedactionConfig, - PiiRedactionProfile, validate_config, ) @@ -30,29 +29,6 @@ def test_defaults_and_component_wrapper(self): "unsupported_value": "error", } assert LocalModelConfig().to_dict() == {} - assert LocalModelConfig( - backend="acme.pii/detector", - model_id="pii-model-v1", - detector_profile="default", - target_paths=["/message"], - target_path_patterns=["/messages/*/content"], - min_score=0.6, - excluded_labels=["CITY"], - replacement="[PRIVATE]", - allow_network=False, - max_latency_ms=250, - ).to_dict() == { - "backend": "acme.pii/detector", - "model_id": "pii-model-v1", - "detector_profile": "default", - "target_paths": ["/message"], - "target_path_patterns": ["/messages/*/content"], - "min_score": 0.6, - "excluded_labels": ["CITY"], - "replacement": "[PRIVATE]", - "allow_network": False, - "max_latency_ms": 250, - } wrapped = ComponentSpec(PiiRedactionConfig()).to_dict() assert wrapped["kind"] == PII_REDACTION_PLUGIN_KIND @@ -66,36 +42,6 @@ def test_defaults_and_component_wrapper(self): opted_out = PiiRedactionConfig(mark=False).to_dict() assert opted_out["mark"] is False - def test_profile_config_omits_legacy_top_level_fields(self): - config = PiiRedactionConfig( - codec="openai_chat", - profiles=[ - PiiRedactionProfile( - mode="builtin", - builtin=BuiltinConfig(detector="email"), - ), - PiiRedactionProfile( - mode="local_model", - priority=110, - local=LocalModelConfig( - backend="acme.pii/detector", - target_path_patterns=["/messages/*/content"], - ), - ), - ], - ).to_dict() - - profiles = config["profiles"] - assert isinstance(profiles, list) - local_profile = profiles[1] - assert isinstance(local_profile, dict) - local = local_profile["local"] - assert isinstance(local, dict) - assert local["backend"] == "acme.pii/detector" - assert "mode" not in config - assert "input" not in config - assert validate_config(config)["diagnostics"] == [] - def test_validation_rejects_bad_values(self): report = validate_config( PiiRedactionConfig( From c082ee754df6f36f21ce589b66a23c1444b39e1b Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 27 Jul 2026 14:07:56 -0700 Subject: [PATCH 11/83] feat(pii): add in-process Rampart plugin Signed-off-by: Alex Fournier --- ATTRIBUTIONS-Rust.md | 33860 ++++++++++------ Cargo.lock | 611 +- Cargo.toml | 2 +- crates/cli/src/plugins/editor_model.rs | 103 + crates/cli/src/plugins/mod.rs | 4 + crates/cli/src/server/mod.rs | 11 +- .../tests/coverage/shared/plugins_tests.rs | 66 + crates/ffi/src/api/plugin.rs | 17 + crates/node/package.json | 4 + crates/node/pii_rampart.d.ts | 38 + crates/node/pii_rampart.js | 71 + crates/node/src/api/mod.rs | 3 + crates/node/tests/pii_rampart_tests.mjs | 39 + crates/pii-redaction/Cargo.toml | 5 + crates/pii-redaction/src/builtin.rs | 2 +- crates/pii-redaction/src/lib.rs | 2 + crates/pii-redaction/src/rampart/mod.rs | 703 + crates/pii-redaction/src/rampart/model.rs | 704 + crates/pii-redaction/src/rampart/sanitizer.rs | 737 + crates/pii-redaction/src/rampart/tokenizer.rs | 333 + crates/python/src/lib.rs | 6 + go/nemo_relay/pii_rampart.go | 72 + go/nemo_relay/pii_rampart/pii_rampart.go | 34 + go/nemo_relay/pii_rampart/pii_rampart_test.go | 22 + go/nemo_relay/pii_rampart_test.go | 43 + python/nemo_relay/__init__.py | 3 + python/nemo_relay/__init__.pyi | 1 + python/nemo_relay/pii_rampart.py | 105 + python/nemo_relay/pii_rampart.pyi | 46 + python/tests/test_pii_rampart_plugin.py | 42 + 30 files changed, 25054 insertions(+), 12635 deletions(-) create mode 100644 crates/node/pii_rampart.d.ts create mode 100644 crates/node/pii_rampart.js create mode 100644 crates/node/tests/pii_rampart_tests.mjs create mode 100644 crates/pii-redaction/src/rampart/mod.rs create mode 100644 crates/pii-redaction/src/rampart/model.rs create mode 100644 crates/pii-redaction/src/rampart/sanitizer.rs create mode 100644 crates/pii-redaction/src/rampart/tokenizer.rs create mode 100644 go/nemo_relay/pii_rampart.go create mode 100644 go/nemo_relay/pii_rampart/pii_rampart.go create mode 100644 go/nemo_relay/pii_rampart/pii_rampart_test.go create mode 100644 go/nemo_relay/pii_rampart_test.go create mode 100644 python/nemo_relay/pii_rampart.py create mode 100644 python/nemo_relay/pii_rampart.pyi create mode 100644 python/tests/test_pii_rampart_plugin.py diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index 2821c2c15..1a87bae30 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -9,6 +9,215 @@ This project uses the following third-party libraries. Each library is open-sour This file is automatically generated. Please do not edit it directly. Regenerate with `./scripts/generate_attributions.sh rust`. +## adler2 - 2.0.1 +**Repository URL**: https://github.com/oyvindln/adler2 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + ## ahash - 0.8.12 **Repository URL**: https://github.com/tkaitchuck/ahash **License Type(s)**: Apache-2.0 @@ -1540,6 +1749,87 @@ limitations under the License. ``` +## anymap3 - 1.1.0 +**Repository URL**: https://github.com/reivilibre/anymap3 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + ## arc-swap - 1.9.1 **Repository URL**: https://github.com/vorner/arc-swap **License Type(s)**: Apache-2.0 @@ -3411,6 +3701,215 @@ limitations under the License. ``` +## bit-set - 0.10.0 +**Repository URL**: https://github.com/contain-rs/bit-set +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + ## bit-set - 0.8.0 **Repository URL**: https://github.com/contain-rs/bit-set **License Type(s)**: Apache-2.0 @@ -3829,6 +4328,215 @@ limitations under the License. ``` +## bit-vec - 0.9.1 +**Repository URL**: https://github.com/contain-rs/bit-vec +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + ## bitflags - 2.11.0 **Repository URL**: https://github.com/bitflags/bitflags **License Type(s)**: Apache-2.0 @@ -5037,6 +5745,35 @@ limitations under the License. ``` +## byteorder - 1.5.0 +**Repository URL**: https://github.com/BurntSushi/byteorder +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + ## bytes - 1.11.1 **Repository URL**: https://github.com/tokio-rs/bytes **License Type(s)**: MIT @@ -8974,6 +9711,216 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +``` + +## crc32fast - 1.5.0 +**Repository URL**: https://github.com/srijs/rust-crc32fast +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ``` ## crossbeam - 0.8.4 @@ -10230,6 +11177,35 @@ limitations under the License. ``` +## crunchy - 0.2.4 +**Repository URL**: https://github.com/eira-fransham/crunchy +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright 2017-2023 Eira Fransham. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ## crypto-common - 0.1.7 **Repository URL**: https://github.com/RustCrypto/traits **License Type(s)**: Apache-2.0 @@ -11097,6 +12073,35 @@ SOFTWARE. ``` +## derive-new - 0.7.0 +**Repository URL**: https://github.com/nrc/derive-new +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2017-2021 nrc (Nick Cameron) and the derive-new contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ## dialoguer - 0.11.0 **Repository URL**: https://github.com/console-rs/dialoguer **License Type(s)**: MIT @@ -11754,89 +12759,8 @@ limitations under the License. ``` -## dyn-clone - 1.0.20 -**Repository URL**: https://github.com/dtolnay/dyn-clone -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## either - 1.15.0 -**Repository URL**: https://github.com/rayon-rs/either +## downcast-rs - 2.0.2 +**Repository URL**: https://github.com/marcianx/downcast-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -12034,7 +12958,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -12044,891 +12968,532 @@ limitations under the License. ``` -## email_address - 0.2.9 -**Repository URL**: https://github.com/johnstonskj/rust-email_address.git -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2019 Simon Johnston - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## encode_unicode - 1.0.0 -**Repository URL**: https://github.com/tormol/encode_unicode +## dyn-clone - 1.0.20 +**Repository URL**: https://github.com/dtolnay/dyn-clone **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - 1. Definitions. +1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS +END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. +APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] +Copyright [yyyy] [name of copyright owner] - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## encoding_rs - 0.8.35 -**Repository URL**: https://github.com/hsivonen/encoding_rs -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## dyn-eq - 0.1.3 +**Repository URL**: https://github.com/Rayzeq/dyn-eq +**License Type(s)**: MPL-2.0 +### License: https://spdx.org/licenses/MPL-2.0.html ``` +Mozilla Public License Version 2.0 +================================== - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +1. Definitions +-------------- - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. - 1. Definitions. +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +1.3. "Contribution" + means Covered Software of a particular Contributor. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +1.5. "Incompatible With Secondary Licenses" + means - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +1.6. "Executable Form" + means any form of the work other than Source Code Form. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +1.8. "License" + means this document. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +1.10. "Modifications" + means any of the following: - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + (b) any new file in Source Code Form that contains any Covered + Software. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +1.13. "Source Code Form" + means the form of the work preferred for making modifications. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +2. License Grants and Conditions +-------------------------------- - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +2.1. Grants - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +2.2. Effective Date - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. - END OF TERMS AND CONDITIONS +2.3. Limitations on Grant Scope - APPENDIX: How to apply the Apache License to your work. +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +(a) for any code that a Contributor has removed from Covered Software; + or - Copyright [yyyy] [name of copyright owner] +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. - http://www.apache.org/licenses/LICENSE-2.0 +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +2.4. Subsequent Licenses -``` +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). -## encoding_rs - 0.8.35 -**Repository URL**: https://github.com/hsivonen/encoding_rs -**License Type(s)**: BSD-3-Clause -### License: https://spdx.org/licenses/BSD-3-Clause.html -``` -Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). +2.5. Representation -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +2.6. Fair Use -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. +2.7. Conditions -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. -``` +3. Responsibilities +------------------- -## env_filter - 0.1.4 -**Repository URL**: https://github.com/rust-cli/env_logger -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +3.1. Distribution of Source Form - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. - 1. Definitions. +3.2. Distribution of Executable Form - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +If You distribute Covered Software in Executable Form then: - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +3.3. Distribution of a Larger Work - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +3.4. Notices - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +3.5. Application of Additional Terms - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +5. Termination +-------------- - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +8. Litigation +------------- - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +9. Miscellaneous +---------------- - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +10. Versions of the License +--------------------------- - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +10.1. New Versions - END OF TERMS AND CONDITIONS +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. - APPENDIX: How to apply the Apache License to your work. +10.2. Effect of New Versions - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. - Copyright {yyyy} {name of copyright owner} +10.3. Modified Versions - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). - http://www.apache.org/licenses/LICENSE-2.0 +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. +Exhibit A - Source Code Form License Notice +------------------------------------------- -``` + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. -## equivalent - 1.0.2 -**Repository URL**: https://github.com/indexmap-rs/equivalent -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +``` + +## dyn-hash - 1.0.0 +**Repository URL**: https://github.com/dtolnay/dyn-hash +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -12936,7 +13501,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -12946,219 +13511,8 @@ limitations under the License. ``` -## erased-serde - 0.4.10 -**Repository URL**: https://github.com/dtolnay/erased-serde -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS -``` - -### License File: LICENSE-MIT -``` -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -``` - -## errno - 0.3.14 -**Repository URL**: https://github.com/lambda-fairy/rust-errno +## either - 1.15.0 +**Repository URL**: https://github.com/rayon-rs/either **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -13366,455 +13720,701 @@ limitations under the License. ``` -## event-listener - 5.4.1 -**Repository URL**: https://github.com/smol-rs/event-listener +## email_address - 0.2.9 +**Repository URL**: https://github.com/johnstonskj/rust-email_address.git +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2019 Simon Johnston + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## encode_unicode - 1.0.0 +**Repository URL**: https://github.com/tormol/encode_unicode **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -1. Definitions. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + 1. Definitions. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -END OF TERMS AND CONDITIONS + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -APPENDIX: How to apply the Apache License to your work. + END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + APPENDIX: How to apply the Apache License to your work. -Copyright [yyyy] [name of copyright owner] + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Copyright [yyyy] [name of copyright owner] - http://www.apache.org/licenses/LICENSE-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ``` -## event-listener-strategy - 0.5.4 -**Repository URL**: https://github.com/smol-rs/event-listener-strategy +## encoding_rs - 0.8.35 +**Repository URL**: https://github.com/hsivonen/encoding_rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -1. Definitions. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + 1. Definitions. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -END OF TERMS AND CONDITIONS + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -APPENDIX: How to apply the Apache License to your work. + END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + APPENDIX: How to apply the Apache License to your work. -Copyright [yyyy] [name of copyright owner] + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Copyright [yyyy] [name of copyright owner] - http://www.apache.org/licenses/LICENSE-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ``` -## fancy-regex - 0.18.0 -**Repository URL**: https://github.com/fancy-regex/fancy-regex -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## encoding_rs - 0.8.35 +**Repository URL**: https://github.com/hsivonen/encoding_rs +**License Type(s)**: BSD-3-Clause +### License: https://spdx.org/licenses/BSD-3-Clause.html ``` -The MIT License +Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). -Copyright 2015 The Fancy Regex Authors. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` -## fastrand - 2.4.1 -**Repository URL**: https://github.com/smol-rs/fastrand +## env_filter - 0.1.4 +**Repository URL**: https://github.com/rust-cli/env_logger +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +``` + +## equivalent - 1.0.2 +**Repository URL**: https://github.com/indexmap-rs/equivalent **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14022,8 +14622,89 @@ limitations under the License. ``` -## find-msvc-tools - 0.1.9 -**Repository URL**: https://github.com/rust-lang/cc-rs +## erased-serde - 0.4.10 +**Repository URL**: https://github.com/dtolnay/erased-serde +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## errno - 0.3.14 +**Repository URL**: https://github.com/lambda-fairy/rust-errno **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14231,8 +14912,8 @@ limitations under the License. ``` -## fixedbitset - 0.5.7 -**Repository URL**: https://github.com/petgraph/fixedbitset +## event-listener - 5.4.1 +**Repository URL**: https://github.com/smol-rs/event-listener **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14440,36 +15121,8 @@ limitations under the License. ``` -## fluent-uri - 0.4.1 -**Repository URL**: https://github.com/yescallop/fluent-uri-rs -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2024 Scallop Ye - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## fnv - 1.0.7 -**Repository URL**: https://github.com/servo/rust-fnv +## event-listener-strategy - 0.5.4 +**Repository URL**: https://github.com/smol-rs/event-listener-strategy **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14677,60 +15330,37 @@ limitations under the License. ``` -## foldhash - 0.1.5 -**Repository URL**: https://github.com/orlp/foldhash -**License Type(s)**: Zlib -### License: https://spdx.org/licenses/Zlib.html +## fancy-regex - 0.18.0 +**Repository URL**: https://github.com/fancy-regex/fancy-regex +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Copyright (c) 2024 Orson Peters +The MIT License -This software is provided 'as-is', without any express or implied warranty. In -no event will the authors be held liable for any damages arising from the use of -this software. +Copyright 2015 The Fancy Regex Authors. -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -1. The origin of this software must not be misrepresented; you must not claim - that you wrote the original software. If you use this software in a product, - an acknowledgment in the product documentation would be appreciated but is - not required. - -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. -``` - -## foldhash - 0.2.0 -**Repository URL**: https://github.com/orlp/foldhash -**License Type(s)**: Zlib -### License: https://spdx.org/licenses/Zlib.html -``` -Copyright (c) 2024 Orson Peters - -This software is provided 'as-is', without any express or implied warranty. In -no event will the authors be held liable for any damages arising from the use of -this software. - -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim - that you wrote the original software. If you use this software in a product, - an acknowledgment in the product documentation would be appreciated but is - not required. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. -3. This notice may not be removed or altered from any source distribution. ``` -## form_urlencoded - 1.2.2 -**Repository URL**: https://github.com/servo/rust-url +## fastrand - 2.4.1 +**Repository URL**: https://github.com/smol-rs/fastrand **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14938,8 +15568,8 @@ limitations under the License. ``` -## fraction - 0.15.4 -**Repository URL**: https://github.com/dnsl48/fraction.git +## filetime - 0.2.29 +**Repository URL**: https://github.com/alexcrichton/filetime **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15147,8 +15777,8 @@ limitations under the License. ``` -## fs2 - 0.4.3 -**Repository URL**: https://github.com/danburkert/fs2-rs +## find-msvc-tools - 0.1.9 +**Repository URL**: https://github.com/rust-lang/cc-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15356,8 +15986,8 @@ limitations under the License. ``` -## futures - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## fixedbitset - 0.5.7 +**Repository URL**: https://github.com/petgraph/fixedbitset **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15549,8 +16179,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15566,8 +16195,8 @@ limitations under the License. ``` -## futures-channel - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## flate2 - 1.1.9 +**Repository URL**: https://github.com/rust-lang/flate2-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15759,8 +16388,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15776,8 +16404,246 @@ limitations under the License. ``` -## futures-core - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## float-ord - 0.3.2 +**Repository URL**: https://github.com/notriddle/rust-float-ord +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +``` + +## fluent-uri - 0.4.1 +**Repository URL**: https://github.com/yescallop/fluent-uri-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2024 Scallop Ye + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## fnv - 1.0.7 +**Repository URL**: https://github.com/servo/rust-fnv **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15969,8 +16835,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15986,218 +16851,60 @@ limitations under the License. ``` -## futures-executor - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## foldhash - 0.1.5 +**Repository URL**: https://github.com/orlp/foldhash +**License Type(s)**: Zlib +### License: https://spdx.org/licenses/Zlib.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +Copyright (c) 2024 Orson Peters -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. -END OF TERMS AND CONDITIONS +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. -APPENDIX: How to apply the Apache License to your work. +3. This notice may not be removed or altered from any source distribution. +``` - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +## foldhash - 0.2.0 +**Repository URL**: https://github.com/orlp/foldhash +**License Type(s)**: Zlib +### License: https://spdx.org/licenses/Zlib.html +``` +Copyright (c) 2024 Orson Peters -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: - http://www.apache.org/licenses/LICENSE-2.0 +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. ``` -## futures-io - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## form_urlencoded - 1.2.2 +**Repository URL**: https://github.com/servo/rust-url **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16389,8 +17096,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16406,8 +17112,8 @@ limitations under the License. ``` -## futures-macro - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## fraction - 0.15.4 +**Repository URL**: https://github.com/dnsl48/fraction.git **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16599,8 +17305,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16616,8 +17321,8 @@ limitations under the License. ``` -## futures-sink - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## fs2 - 0.4.3 +**Repository URL**: https://github.com/danburkert/fs2-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16809,8 +17514,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16826,7 +17530,7 @@ limitations under the License. ``` -## futures-task - 0.3.32 +## futures - 0.3.32 **Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html @@ -17036,7 +17740,7 @@ limitations under the License. ``` -## futures-util - 0.3.32 +## futures-channel - 0.3.32 **Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html @@ -17246,42 +17950,224 @@ limitations under the License. ``` -## generic-array - 0.14.7 -**Repository URL**: https://github.com/fizyk20/generic-array.git -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## futures-core - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -The MIT License (MIT) - -Copyright (c) 2015 Bartłomiej Kamiński - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + ``` -## getrandom - 0.2.17 -**Repository URL**: https://github.com/rust-random/getrandom +## futures-executor - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -17467,13 +18353,14 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17483,14 +18370,14 @@ limitations under the License. ``` -## getrandom - 0.3.4 -**Repository URL**: https://github.com/rust-random/getrandom +## futures-io - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -17676,13 +18563,14 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17692,14 +18580,14 @@ limitations under the License. ``` -## getrandom - 0.4.2 -**Repository URL**: https://github.com/rust-random/getrandom +## futures-macro - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -17885,249 +18773,8 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## h2 - 0.4.13 -**Repository URL**: https://github.com/hyperium/h2 -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2017 h2 authors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -``` - -## hashbrown - 0.15.5 -**Repository URL**: https://github.com/rust-lang/hashbrown -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18143,8 +18790,8 @@ limitations under the License. ``` -## hashbrown - 0.16.1 -**Repository URL**: https://github.com/rust-lang/hashbrown +## futures-sink - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18336,7 +18983,8 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18352,8 +19000,8 @@ limitations under the License. ``` -## hashbrown - 0.17.0 -**Repository URL**: https://github.com/rust-lang/hashbrown +## futures-task - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18545,7 +19193,8 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18561,8 +19210,8 @@ limitations under the License. ``` -## heck - 0.5.0 -**Repository URL**: https://github.com/withoutboats/heck +## futures-util - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18754,7 +19403,8 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18770,14 +19420,42 @@ limitations under the License. ``` -## http - 1.4.0 -**Repository URL**: https://github.com/hyperium/http +## generic-array - 0.14.7 +**Repository URL**: https://github.com/fizyk20/generic-array.git +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2015 Bartłomiej Kamiński + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## getrandom - 0.2.17 +**Repository URL**: https://github.com/rust-random/getrandom **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -18963,13 +19641,13 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2017 http-rs authors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18979,80 +19657,14 @@ limitations under the License. ``` -## http-body - 1.0.1 -**Repository URL**: https://github.com/hyperium/http-body -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019-2024 Sean McArthur & Hyper Contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -``` - -## http-body-util - 0.1.3 -**Repository URL**: https://github.com/hyperium/http-body -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019-2025 Sean McArthur & Hyper Contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -``` - -## httparse - 1.10.1 -**Repository URL**: https://github.com/seanmonstar/httparse +## getrandom - 0.3.4 +**Repository URL**: https://github.com/rust-random/getrandom **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -19244,7 +19856,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19254,198 +19866,198 @@ limitations under the License. ``` -## httpdate - 1.0.3 -**Repository URL**: https://github.com/pyfisch/httpdate +## getrandom - 0.4.2 +**Repository URL**: https://github.com/rust-random/getrandom **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "[]" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -19453,7 +20065,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19463,218 +20075,122 @@ limitations under the License. ``` -## humantime - 2.3.0 -**Repository URL**: https://github.com/chronotope/humantime -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## h2 - 0.4.13 +**Repository URL**: https://github.com/hyperium/h2 +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Copyright (c) 2017 h2 authors - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: - 1. Definitions. +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +``` - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +## half - 2.7.1 +**Repository URL**: https://github.com/VoidStarKat/half-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +1. Definitions. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - END OF TERMS AND CONDITIONS +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - APPENDIX: How to apply the Apache License to your work. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - Copyright {yyyy} {name of copyright owner} +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - http://www.apache.org/licenses/LICENSE-2.0 +END OF TERMS AND CONDITIONS - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## hybrid-array - 0.4.10 -**Repository URL**: https://github.com/RustCrypto/hybrid-array +## hashbrown - 0.15.5 +**Repository URL**: https://github.com/rust-lang/hashbrown **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -19872,7 +20388,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19882,35 +20398,217 @@ limitations under the License. ``` -## hyper - 1.9.0 -**Repository URL**: https://github.com/hyperium/hyper -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## hashbrown - 0.16.1 +**Repository URL**: https://github.com/rust-lang/hashbrown +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -Copyright (c) 2014-2026 Sean McArthur + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## hyper-rustls - 0.27.9 -**Repository URL**: https://github.com/rustls/hyper-rustls +## hashbrown - 0.17.0 +**Repository URL**: https://github.com/rust-lang/hashbrown **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -20118,8 +20816,8 @@ limitations under the License. ``` -## hyper-timeout - 0.5.2 -**Repository URL**: https://github.com/hjr3/hyper-timeout +## heck - 0.5.0 +**Repository URL**: https://github.com/withoutboats/heck **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -20327,35 +21025,8 @@ limitations under the License. ``` -## hyper-util - 0.1.20 -**Repository URL**: https://github.com/hyperium/hyper-util -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2023-2025 Sean McArthur - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -``` - -## iana-time-zone - 0.1.65 -**Repository URL**: https://github.com/strawlab/iana-time-zone +## http - 1.4.0 +**Repository URL**: https://github.com/hyperium/http **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -20547,7 +21218,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2020 Andrew Straw +Copyright 2017 http-rs authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20563,8 +21234,74 @@ limitations under the License. ``` -## iana-time-zone-haiku - 0.1.2 -**Repository URL**: https://github.com/strawlab/iana-time-zone +## http-body - 1.0.1 +**Repository URL**: https://github.com/hyperium/http-body +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019-2024 Sean McArthur & Hyper Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## http-body-util - 0.1.3 +**Repository URL**: https://github.com/hyperium/http-body +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019-2025 Sean McArthur & Hyper Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## httparse - 1.10.1 +**Repository URL**: https://github.com/seanmonstar/httparse **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -20756,7 +21493,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2020 Andrew Straw +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20772,389 +21509,429 @@ limitations under the License. ``` -## icu_collections - 2.2.0 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html +## httpdate - 1.0.3 +**Repository URL**: https://github.com/pyfisch/httpdate +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -UNICODE LICENSE V3 - -COPYRIGHT AND PERMISSION NOTICE - -Copyright © 2020-2024 Unicode, Inc. +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. +1. Definitions. -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. -SPDX-License-Identifier: Unicode-3.0 +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. -— +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. -``` +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). -## icu_locale_core - 2.2.0 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html -``` -UNICODE LICENSE V3 +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. -COPYRIGHT AND PERMISSION NOTICE +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." -Copyright © 2020-2024 Unicode, Inc. +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and -SPDX-License-Identifier: Unicode-3.0 +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and -— +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. -``` +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. -## icu_normalizer - 2.2.0 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html -``` -UNICODE LICENSE V3 +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. -COPYRIGHT AND PERMISSION NOTICE +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. -Copyright © 2020-2024 Unicode, Inc. +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. +END OF TERMS AND CONDITIONS -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. +APPENDIX: How to apply the Apache License to your work. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. +Copyright [yyyy] [name of copyright owner] -SPDX-License-Identifier: Unicode-3.0 +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -— +http://www.apache.org/licenses/LICENSE-2.0 -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## icu_normalizer_data - 2.2.0 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html +## humantime - 2.3.0 +**Repository URL**: https://github.com/chronotope/humantime +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -UNICODE LICENSE V3 - -COPYRIGHT AND PERMISSION NOTICE - -Copyright © 2020-2024 Unicode, Inc. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. + 1. Definitions. -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -SPDX-License-Identifier: Unicode-3.0 + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -— + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -``` + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -## icu_properties - 2.2.0 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html -``` -UNICODE LICENSE V3 + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -COPYRIGHT AND PERMISSION NOTICE + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -Copyright © 2020-2024 Unicode, Inc. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. - -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. - -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. - -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. - -SPDX-License-Identifier: Unicode-3.0 - -— - -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - -``` - -## icu_properties_data - 2.2.0 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html -``` -UNICODE LICENSE V3 - -COPYRIGHT AND PERMISSION NOTICE - -Copyright © 2020-2024 Unicode, Inc. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and -SPDX-License-Identifier: Unicode-3.0 + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -— + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -``` + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -## icu_provider - 2.2.0 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html -``` -UNICODE LICENSE V3 + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -COPYRIGHT AND PERMISSION NOTICE + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -Copyright © 2020-2024 Unicode, Inc. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + END OF TERMS AND CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. + APPENDIX: How to apply the Apache License to your work. -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. + Copyright {yyyy} {name of copyright owner} -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -SPDX-License-Identifier: Unicode-3.0 + http://www.apache.org/licenses/LICENSE-2.0 -— + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. ``` -## id-arena - 2.3.0 -**Repository URL**: https://github.com/fitzgen/id-arena -**License Type(s)**: MIT/Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## hybrid-array - 0.4.10 +**Repository URL**: https://github.com/RustCrypto/hybrid-array +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -21350,46 +22127,45 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + ``` -### License File: LICENSE-MIT +## hyper - 1.9.0 +**Repository URL**: https://github.com/hyperium/hyper +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Copyright (c) 2014 Alex Crichton +Copyright (c) 2014-2026 Sean McArthur -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ``` -## idna - 1.1.0 -**Repository URL**: https://github.com/servo/rust-url/ +## hyper-rustls - 0.27.9 +**Repository URL**: https://github.com/rustls/hyper-rustls **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -21597,8 +22373,8 @@ limitations under the License. ``` -## idna_adapter - 1.2.1 -**Repository URL**: https://github.com/hsivonen/idna_adapter +## hyper-timeout - 0.5.2 +**Repository URL**: https://github.com/hjr3/hyper-timeout **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -21806,8 +22582,35 @@ limitations under the License. ``` -## indexmap - 2.14.0 -**Repository URL**: https://github.com/indexmap-rs/indexmap +## hyper-util - 0.1.20 +**Repository URL**: https://github.com/hyperium/hyper-util +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2023-2025 Sean McArthur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + +## iana-time-zone - 0.1.65 +**Repository URL**: https://github.com/strawlab/iana-time-zone **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -21999,7 +22802,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright 2020 Andrew Straw Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22015,667 +22818,626 @@ limitations under the License. ``` -## ipnet - 2.12.0 -**Repository URL**: https://github.com/krisprice/ipnet +## iana-time-zone-haiku - 0.1.2 +**Repository URL**: https://github.com/strawlab/iana-time-zone **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - 1. Definitions. +1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS +END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. +APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - Copyright 2017 Juniper Networks, Inc. +Copyright 2020 Andrew Straw - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## iri-string - 0.7.12 -**Repository URL**: https://github.com/lo48576/iri-string -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## icu_collections - 2.2.0 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html ``` +UNICODE LICENSE V3 - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +COPYRIGHT AND PERMISSION NOTICE - 1. Definitions. +Copyright © 2020-2024 Unicode, Inc. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +SPDX-License-Identifier: Unicode-3.0 - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +— - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +``` - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +## icu_locale_core - 2.2.0 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html +``` +UNICODE LICENSE V3 - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +COPYRIGHT AND PERMISSION NOTICE - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +Copyright © 2020-2024 Unicode, Inc. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +SPDX-License-Identifier: Unicode-3.0 - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +— - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +``` - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +## icu_normalizer - 2.2.0 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html +``` +UNICODE LICENSE V3 - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +COPYRIGHT AND PERMISSION NOTICE - END OF TERMS AND CONDITIONS +Copyright © 2020-2024 Unicode, Inc. - APPENDIX: How to apply the Apache License to your work. +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - Copyright [yyyy] [name of copyright owner] +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. - http://www.apache.org/licenses/LICENSE-2.0 +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. ``` -## is_terminal_polyfill - 1.70.2 -**Repository URL**: https://github.com/polyfill-rs/is_terminal_polyfill -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## icu_normalizer_data - 2.2.0 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +UNICODE LICENSE V3 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +COPYRIGHT AND PERMISSION NOTICE - 1. Definitions. +Copyright © 2020-2024 Unicode, Inc. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +SPDX-License-Identifier: Unicode-3.0 - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +— - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +``` - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +## icu_properties - 2.2.0 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html +``` +UNICODE LICENSE V3 - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +COPYRIGHT AND PERMISSION NOTICE - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +Copyright © 2020-2024 Unicode, Inc. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +SPDX-License-Identifier: Unicode-3.0 - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +— - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +``` - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +## icu_properties_data - 2.2.0 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html +``` +UNICODE LICENSE V3 - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +COPYRIGHT AND PERMISSION NOTICE - END OF TERMS AND CONDITIONS +Copyright © 2020-2024 Unicode, Inc. - APPENDIX: How to apply the Apache License to your work. +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - Copyright {yyyy} {name of copyright owner} +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. - http://www.apache.org/licenses/LICENSE-2.0 +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +SPDX-License-Identifier: Unicode-3.0 +— -``` +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. -## itertools - 0.14.0 -**Repository URL**: https://github.com/rust-itertools/itertools -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +## icu_provider - 2.2.0 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html +``` +UNICODE LICENSE V3 -1. Definitions. +COPYRIGHT AND PERMISSION NOTICE - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +Copyright © 2020-2024 Unicode, Inc. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + +``` + +## id-arena - 2.3.0 +**Repository URL**: https://github.com/fitzgen/id-arena +**License Type(s)**: MIT/Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical @@ -22850,73 +23612,229 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +``` +### License File: LICENSE-MIT ``` +Copyright (c) 2014 Alex Crichton -## itoa - 1.0.18 -**Repository URL**: https://github.com/dtolnay/itoa +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` + +## idna - 1.1.0 +**Repository URL**: https://github.com/servo/rust-url/ **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -22924,7 +23842,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -22934,8 +23852,8 @@ limitations under the License. ``` -## jobserver - 0.1.35 -**Repository URL**: https://github.com/rust-lang/jobserver-rs +## idna_adapter - 1.2.1 +**Repository URL**: https://github.com/hsivonen/idna_adapter **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23143,8 +24061,8 @@ limitations under the License. ``` -## js-sys - 0.3.95 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys +## indexmap - 2.14.0 +**Repository URL**: https://github.com/indexmap-rs/indexmap **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23352,482 +24270,8 @@ limitations under the License. ``` -## jsonschema - 0.46.8 -**Repository URL**: https://github.com/Stranger6667/jsonschema -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2020-2026 Dmitry Dygalo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## lazy_static - 1.5.0 -**Repository URL**: https://github.com/rust-lang-nursery/lazy-static.rs -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## leb128fmt - 0.1.0 -**Repository URL**: https://github.com/bluk/leb128fmt -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -``` - -### License File: LICENSE-MIT -``` -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -``` - -## libc - 0.2.185 -**Repository URL**: https://github.com/rust-lang/libc +## inventory - 0.3.24 +**Repository URL**: https://github.com/dtolnay/inventory **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23907,337 +24351,683 @@ limitations under the License. ``` -## libloading - 0.8.9 -**Repository URL**: https://github.com/nagisa/rust_libloading/ -**License Type(s)**: ISC -### License: https://spdx.org/licenses/ISC.html -``` -Copyright © 2015, Simonas Kazlauskas - -Permission to use, copy, modify, and/or distribute this software for any purpose with or without -fee is hereby granted, provided that the above copyright notice and this permission notice appear -in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS -SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE -AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. - -``` - -## linux-raw-sys - 0.12.1 -**Repository URL**: https://github.com/sunfishcode/linux-raw-sys +## ipnet - 2.12.0 +**Repository URL**: https://github.com/krisprice/ipnet **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. + 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. + APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright [yyyy] [name of copyright owner] + Copyright 2017 Juniper Networks, Inc. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ``` -## litemap - 0.8.2 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html +## iri-string - 0.7.12 +**Repository URL**: https://github.com/lo48576/iri-string +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -UNICODE LICENSE V3 - -COPYRIGHT AND PERMISSION NOTICE -Copyright © 2020-2024 Unicode, Inc. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. + 1. Definitions. -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -SPDX-License-Identifier: Unicode-3.0 + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -— + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -``` + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -## lock_api - 0.4.14 -**Repository URL**: https://github.com/Amanieu/parking_lot -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -1. Definitions. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +``` + +## is_terminal_polyfill - 1.70.2 +**Repository URL**: https://github.com/polyfill-rs/is_terminal_polyfill +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +``` + +## itertools - 0.14.0 +**Repository URL**: https://github.com/rust-itertools/itertools +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. @@ -24399,8 +25189,89 @@ limitations under the License. ``` -## log - 0.4.29 -**Repository URL**: https://github.com/rust-lang/log +## itoa - 1.0.18 +**Repository URL**: https://github.com/dtolnay/itoa +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## jobserver - 0.1.35 +**Repository URL**: https://github.com/rust-lang/jobserver-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24608,368 +25479,57 @@ limitations under the License. ``` -## lru-slab - 0.1.2 -**Repository URL**: https://github.com/Ralith/lru-slab -**License Type(s)**: MIT OR Apache-2.0 OR Zlib -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## js-sys - 0.3.95 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - 1. Definitions. +1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -``` - -### License File: LICENSE-MIT -``` -Copyright (c) 2024 The lru-slab Developers - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` - -### License File: LICENSE-ZLIB -``` -Copyright (c) 2024 The lru-slab Developers - -This software is provided 'as-is', without any express or implied warranty. In -no event will the authors be held liable for any damages arising from the use of -this software. - -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim - that you wrote the original software. If you use this software in a product, an - acknowledgment in the product documentation would be appreciated but is not - required. - -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. -``` - -## matchit - 0.8.4 -**Repository URL**: https://github.com/ibraheemdev/matchit -**License Type(s)**: BSD-3-Clause -### License: https://spdx.org/licenses/BSD-3-Clause.html -``` -BSD 3-Clause License - -Copyright (c) 2013, Julien Schmidt -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -``` - -## matchit - 0.8.4 -**Repository URL**: https://github.com/ibraheemdev/matchit -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2022 Ibraheem Ahmed - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## md-5 - 0.10.6 -**Repository URL**: https://github.com/RustCrypto/hashes -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions @@ -25118,54 +25678,24 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` - -### License File: LICENSE-MIT -``` -Copyright (c) 2006-2009 Graydon Hoare -Copyright (c) 2009-2013 Mozilla Foundation -Copyright (c) 2016 Artyom Pavlov - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ``` -## memchr - 2.8.0 -**Repository URL**: https://github.com/BurntSushi/memchr +## jsonschema - 0.46.8 +**Repository URL**: https://github.com/Stranger6667/jsonschema **License Type(s)**: MIT ### License: https://spdx.org/licenses/MIT.html ``` -The MIT License (MIT) +MIT License -Copyright (c) 2015 Andrew Gallant +Copyright (c) 2020-2026 Dmitry Dygalo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25174,48 +25704,21 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -``` - -## micromap - 0.3.0 -**Repository URL**: https://github.com/yegor256/micromap -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2023-2026 Yegor Bugayenko - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## mime - 0.3.17 -**Repository URL**: https://github.com/hyperium/mime +## lazy_static - 1.5.0 +**Repository URL**: https://github.com/rust-lang-nursery/lazy-static.rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25423,37 +25926,11 @@ limitations under the License. ``` -## mio - 1.2.0 -**Repository URL**: https://github.com/tokio-rs/mio -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2014 Carl Lerche and other MIO contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -``` - -## multimap - 0.10.1 -**Repository URL**: https://github.com/havarnov/multimap -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## leb128fmt - 0.1.0 +**Repository URL**: https://github.com/bluk/leb128fmt +**License Type(s)**: MIT OR Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -25656,115 +26133,138 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - ``` -## napi - 2.16.17 -**Repository URL**: https://github.com/napi-rs/napi-rs -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +### License File: LICENSE-MIT ``` -MIT License +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: -Copyright (c) +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +## libc - 0.2.185 +**Repository URL**: https://github.com/rust-lang/libc +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -``` +1. Definitions. -## napi-build - 2.3.1 -**Repository URL**: https://github.com/napi-rs/napi-rs -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. -Copyright (c) +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. -``` +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. -## napi-derive - 2.16.13 -**Repository URL**: https://github.com/napi-rs/napi-rs -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). -Copyright (c) +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -``` +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -## napi-derive-backend - 1.0.75 -**Repository URL**: https://github.com/napi-rs/napi-rs -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: -Copyright (c) + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## napi-sys - 2.4.0 -**Repository URL**: https://github.com/napi-rs/napi-rs +## libloading - 0.8.9 +**Repository URL**: https://github.com/nagisa/rust_libloading/ +**License Type(s)**: ISC +### License: https://spdx.org/licenses/ISC.html +``` +Copyright © 2015, Simonas Kazlauskas + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +``` + +## libm - 0.2.16 +**Repository URL**: https://github.com/rust-lang/compiler-builtins **License Type(s)**: MIT ### License: https://spdx.org/licenses/MIT.html ``` @@ -25789,36 +26289,8 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## nom - 8.0.0 -**Repository URL**: https://github.com/rust-bakery/nom -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2014-2019 Geoffroy Couprie - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## num - 0.4.3 -**Repository URL**: https://github.com/rust-num/num +## linux-raw-sys - 0.12.1 +**Repository URL**: https://github.com/sunfishcode/linux-raw-sys **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -26026,8 +26498,62 @@ limitations under the License. ``` -## num-bigint - 0.4.6 -**Repository URL**: https://github.com/rust-num/num-bigint +## litemap - 0.8.2 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html +``` +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + +``` + +## lock_api - 0.4.14 +**Repository URL**: https://github.com/Amanieu/parking_lot **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -26235,89 +26761,8 @@ limitations under the License. ``` -## num-cmp - 0.1.0 -**Repository URL**: https://github.com/lifthrasiir/num-cmp -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## num-complex - 0.4.6 -**Repository URL**: https://github.com/rust-num/num-complex +## log - 0.4.29 +**Repository URL**: https://github.com/rust-lang/log **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -26525,89 +26970,252 @@ limitations under the License. ``` -## num-conv - 0.2.2 -**Repository URL**: https://github.com/jhpratt/num-conv -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## lru-slab - 0.1.2 +**Repository URL**: https://github.com/Ralith/lru-slab +**License Type(s)**: MIT OR Apache-2.0 OR Zlib +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -1. Definitions. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + 1. Definitions. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -END OF TERMS AND CONDITIONS + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -APPENDIX: How to apply the Apache License to your work. + END OF TERMS AND CONDITIONS -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + APPENDIX: How to apply the Apache License to your work. -Copyright [yyyy] [name of copyright owner] + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Copyright [yyyy] [name of copyright owner] -http://www.apache.org/licenses/LICENSE-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ``` -## num-integer - 0.1.46 -**Repository URL**: https://github.com/rust-num/num-integer +### License File: LICENSE-MIT +``` +Copyright (c) 2024 The lru-slab Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +### License File: LICENSE-ZLIB +``` +Copyright (c) 2024 The lru-slab Developers + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. +``` + +## maplit - 1.0.2 +**Repository URL**: https://github.com/bluss/maplit **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -26815,8 +27423,74 @@ limitations under the License. ``` -## num-iter - 0.1.45 -**Repository URL**: https://github.com/rust-num/num-iter +## matchit - 0.8.4 +**Repository URL**: https://github.com/ibraheemdev/matchit +**License Type(s)**: BSD-3-Clause +### License: https://spdx.org/licenses/BSD-3-Clause.html +``` +BSD 3-Clause License + +Copyright (c) 2013, Julien Schmidt +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +## matchit - 0.8.4 +**Repository URL**: https://github.com/ibraheemdev/matchit +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2022 Ibraheem Ahmed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## matrixmultiply - 0.3.11 +**Repository URL**: https://github.com/bluss/matrixmultiply/ **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -27024,10 +27698,11 @@ limitations under the License. ``` -## num-rational - 0.4.2 -**Repository URL**: https://github.com/rust-num/num-rational -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## md-5 - 0.10.6 +**Repository URL**: https://github.com/RustCrypto/hashes +**License Type(s)**: MIT OR Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -27223,18 +27898,77 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +``` +### License File: LICENSE-MIT ``` +Copyright (c) 2006-2009 Graydon Hoare +Copyright (c) 2009-2013 Mozilla Foundation +Copyright (c) 2016 Artyom Pavlov -## num-traits - 0.2.19 -**Repository URL**: https://github.com/rust-num/num-traits +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` + +## memchr - 2.8.0 +**Repository URL**: https://github.com/BurntSushi/memchr +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + +## memmap2 - 0.9.11 +**Repository URL**: https://github.com/RazrFalcon/memmap2-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -27426,7 +28160,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright [2015] [Dan Burkert] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27442,220 +28176,244 @@ limitations under the License. ``` -## object_store - 0.13.2 -**Repository URL**: https://github.com/apache/arrow-rs-object-store +## memo-map - 0.3.3 +**Repository URL**: https://github.com/mitsuhiko/memo-map **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +1. Definitions. - 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +END OF TERMS AND CONDITIONS - END OF TERMS AND CONDITIONS +APPENDIX: How to apply the Apache License to your work. - APPENDIX: How to apply the Apache License to your work. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +Copyright [yyyy] [name of copyright owner] - Copyright [yyyy] [name of copyright owner] +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 - http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +``` + +## micromap - 0.3.0 +**Repository URL**: https://github.com/yegor256/micromap +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2023-2026 Yegor Bugayenko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## once_cell - 1.21.4 -**Repository URL**: https://github.com/matklad/once_cell +## mime - 0.3.17 +**Repository URL**: https://github.com/hyperium/mime **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -27863,218 +28621,217 @@ limitations under the License. ``` -## once_cell_polyfill - 1.70.2 -**Repository URL**: https://github.com/polyfill-rs/once_cell_polyfill +## minijinja - 2.21.0 +**Repository URL**: https://github.com/mitsuhiko/minijinja **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - 1. Definitions. +1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - END OF TERMS AND CONDITIONS +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - APPENDIX: How to apply the Apache License to your work. +END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +APPENDIX: How to apply the Apache License to your work. - Copyright {yyyy} {name of copyright owner} + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Copyright [yyyy] [name of copyright owner] - http://www.apache.org/licenses/LICENSE-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## openinference-semantic-conventions - 0.1.1 -**Repository URL**: https://github.com/cagyirey/openinference-rs +## miniz_oxide - 0.8.9 +**Repository URL**: https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -28154,8 +28911,35 @@ limitations under the License. ``` -## openssl-probe - 0.2.1 -**Repository URL**: https://github.com/rustls/openssl-probe +## mio - 1.2.0 +**Repository URL**: https://github.com/tokio-rs/mio +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2014 Carl Lerche and other MIO contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + +## multimap - 0.10.1 +**Repository URL**: https://github.com/havarnov/multimap **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -28363,541 +29147,205 @@ limitations under the License. ``` -## opentelemetry - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## napi - 2.16.17 +**Repository URL**: https://github.com/napi-rs/napi-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. +MIT License -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. +Copyright (c) -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). +``` -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. +## napi-build - 2.3.1 +**Repository URL**: https://github.com/napi-rs/napi-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." +Copyright (c) -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +``` - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and +## napi-derive - 2.16.13 +**Repository URL**: https://github.com/napi-rs/napi-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and +Copyright (c) - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +``` -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +## napi-derive-backend - 1.0.75 +**Repository URL**: https://github.com/napi-rs/napi-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +Copyright (c) -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -END OF TERMS AND CONDITIONS +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. -APPENDIX: How to apply the Apache License to your work. +``` -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. +## napi-sys - 2.4.0 +**Repository URL**: https://github.com/napi-rs/napi-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License -Copyright [yyyy] [name of copyright owner] +Copyright (c) -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## opentelemetry-http - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http +## ndarray - 0.17.2 +**Repository URL**: https://github.com/rust-ndarray/ndarray **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## opentelemetry-otlp - 0.31.1 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## opentelemetry-proto - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-proto -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## opentelemetry_sdk - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## ordered-float - 2.10.1 -**Repository URL**: https://github.com/reem/rust-ordered-float -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2015 Jonathan Reem - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -``` - -## outref - 0.5.2 -**Repository URL**: https://github.com/Nugine/outref -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2022 Nugine - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## parking - 2.2.1 -**Repository URL**: https://github.com/smol-rs/parking -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, @@ -29038,8 +29486,62 @@ limitations under the License. ``` -## parking_lot - 0.12.5 -**Repository URL**: https://github.com/Amanieu/parking_lot +## nom - 8.0.0 +**Repository URL**: https://github.com/rust-bakery/nom +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2014-2019 Geoffroy Couprie + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## nom-language - 0.1.0 +**Repository URL**: https://github.com/rust-bakery/nom +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## num - 0.4.3 +**Repository URL**: https://github.com/rust-num/num **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -29247,8 +29749,8 @@ limitations under the License. ``` -## parking_lot_core - 0.9.12 -**Repository URL**: https://github.com/Amanieu/parking_lot +## num-bigint - 0.4.6 +**Repository URL**: https://github.com/rust-num/num-bigint **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -29456,37 +29958,89 @@ limitations under the License. ``` -## pem - 3.0.6 -**Repository URL**: https://github.com/jcreekmore/pem-rs.git -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## num-cmp - 0.1.0 +**Repository URL**: https://github.com/lifthrasiir/num-cmp +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -The MIT License (MIT) +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -Copyright (c) 2016 Jonathan Creekmore +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +1. Definitions. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. -``` +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -## percent-encoding - 2.3.2 -**Repository URL**: https://github.com/servo/rust-url/ +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## num-complex - 0.4.6 +**Repository URL**: https://github.com/rust-num/num-complex **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -29694,8 +30248,89 @@ limitations under the License. ``` -## petgraph - 0.8.3 -**Repository URL**: https://github.com/petgraph/petgraph +## num-conv - 0.2.2 +**Repository URL**: https://github.com/jhpratt/num-conv +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## num-integer - 0.1.46 +**Repository URL**: https://github.com/rust-num/num-integer **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -29903,232 +30538,198 @@ limitations under the License. ``` -## pin-project - 1.1.11 -**Repository URL**: https://github.com/taiki-e/pin-project -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## pin-project-internal - 1.1.11 -**Repository URL**: https://github.com/taiki-e/pin-project -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## pin-project-lite - 0.2.17 -**Repository URL**: https://github.com/taiki-e/pin-project-lite +## num-iter - 0.1.45 +**Repository URL**: https://github.com/rust-num/num-iter **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -30136,7 +30737,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -30146,8 +30747,8 @@ limitations under the License. ``` -## pkg-config - 0.3.33 -**Repository URL**: https://github.com/rust-lang/pkg-config-rs +## num-rational - 0.4.2 +**Repository URL**: https://github.com/rust-num/num-rational **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -30355,70 +30956,198 @@ limitations under the License. ``` -## portable-atomic - 1.13.1 -**Repository URL**: https://github.com/taiki-e/portable-atomic +## num-traits - 0.2.19 +**Repository URL**: https://github.com/rust-num/num-traits **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -30426,7 +31155,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -30436,62 +31165,8 @@ limitations under the License. ``` -## potential_utf - 0.1.5 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html -``` -UNICODE LICENSE V3 - -COPYRIGHT AND PERMISSION NOTICE - -Copyright © 2020-2024 Unicode, Inc. - -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. - -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. - -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. - -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. - -SPDX-License-Identifier: Unicode-3.0 - -— - -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - -``` - -## powerfmt - 0.2.0 -**Repository URL**: https://github.com/jhpratt/powerfmt +## object_store - 0.13.2 +**Repository URL**: https://github.com/apache/arrow-rs-object-store **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -30684,7 +31359,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 Jacob Pratt et al. + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -30698,10 +31373,12 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a See the License for the specific language governing permissions and limitations under the License. + + ``` -## ppv-lite86 - 0.2.21 -**Repository URL**: https://github.com/cryptocorrosion/cryptocorrosion +## once_cell - 1.21.4 +**Repository URL**: https://github.com/matklad/once_cell **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -30893,13 +31570,13 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2019 The CryptoCorrosion Contributors +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -30909,89 +31586,218 @@ limitations under the License. ``` -## prettyplease - 0.2.37 -**Repository URL**: https://github.com/dtolnay/prettyplease +## once_cell_polyfill - 1.70.2 +**Repository URL**: https://github.com/polyfill-rs/once_cell_polyfill **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. + 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. + APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright [yyyy] [name of copyright owner] + Copyright {yyyy} {name of copyright owner} -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ``` -## proc-macro2 - 1.0.106 -**Repository URL**: https://github.com/dtolnay/proc-macro2 +## openinference-semantic-conventions - 0.1.1 +**Repository URL**: https://github.com/cagyirey/openinference-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -31071,8 +31877,8 @@ limitations under the License. ``` -## prost - 0.14.3 -**Repository URL**: https://github.com/tokio-rs/prost +## openssl-probe - 0.2.1 +**Repository URL**: https://github.com/rustls/openssl-probe **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -31280,46 +32086,512 @@ limitations under the License. ``` -## prost-build - 0.14.3 -**Repository URL**: https://github.com/tokio-rs/prost +## opentelemetry - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## opentelemetry-http - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## opentelemetry-otlp - 0.31.1 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## opentelemetry-proto - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-proto +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## opentelemetry_sdk - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## ordered-float - 2.10.1 +**Repository URL**: https://github.com/reem/rust-ordered-float +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2015 Jonathan Reem + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## outref - 0.5.2 +**Repository URL**: https://github.com/Nugine/outref +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2022 Nugine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## parking - 2.2.1 +**Repository URL**: https://github.com/smol-rs/parking +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). @@ -31489,8 +32761,8 @@ limitations under the License. ``` -## prost-derive - 0.14.3 -**Repository URL**: https://github.com/tokio-rs/prost +## parking_lot - 0.12.5 +**Repository URL**: https://github.com/Amanieu/parking_lot **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -31698,8 +32970,8 @@ limitations under the License. ``` -## prost-types - 0.14.3 -**Repository URL**: https://github.com/tokio-rs/prost +## parking_lot_core - 0.9.12 +**Repository URL**: https://github.com/Amanieu/parking_lot **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -31907,248 +33179,95 @@ limitations under the License. ``` -## protoc-bin-vendored - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## pastey - 0.2.3 +**Repository URL**: https://github.com/as1100k/pastey +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -MIT License +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -Copyright (c) +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +1. Definitions. -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## protoc-bin-vendored-linux-aarch_64 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## protoc-bin-vendored-linux-ppcle_64 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## protoc-bin-vendored-linux-s390_64 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## protoc-bin-vendored-linux-x86_32 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. -Copyright (c) +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. -``` +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). -## protoc-bin-vendored-linux-x86_64 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. -Copyright (c) +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -``` +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: -## protoc-bin-vendored-macos-aarch_64 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and -Copyright (c) + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -``` +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -## protoc-bin-vendored-macos-x86_64 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -Copyright (c) +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +END OF TERMS AND CONDITIONS -``` +APPENDIX: How to apply the Apache License to your work. -## protoc-bin-vendored-win32 - 3.2.0 -**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) +Copyright [yyyy] [name of copyright owner] -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +http://www.apache.org/licenses/LICENSE-2.0 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## pulldown-cmark - 0.13.4 -**Repository URL**: https://github.com/raphlinus/pulldown-cmark +## pem - 3.0.6 +**Repository URL**: https://github.com/jcreekmore/pem-rs.git **License Type(s)**: MIT ### License: https://spdx.org/licenses/MIT.html ``` -The MIT License +The MIT License (MIT) -Copyright 2015 Google Inc. All rights reserved. +Copyright (c) 2016 Jonathan Creekmore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -32157,292 +33276,420 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## pulldown-cmark-to-cmark - 22.0.0 -**Repository URL**: https://github.com/Byron/pulldown-cmark-to-cmark +## percent-encoding - 2.3.2 +**Repository URL**: https://github.com/servo/rust-url/ **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - 1. Definitions. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +1. Definitions. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - END OF TERMS AND CONDITIONS +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - APPENDIX: How to apply the Apache License to your work. +END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +APPENDIX: How to apply the Apache License to your work. - Copyright 2018 "Sebastian Thiel ", "Dylan Owen ", "Alessandro Ogier ", "Zixian Cai <2891235+caizixian@users.noreply.github.com>" + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Copyright [yyyy] [name of copyright owner] - http://www.apache.org/licenses/LICENSE-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## pyo3 - 0.29.0 -**Repository URL**: https://github.com/pyo3/pyo3 +## petgraph - 0.8.3 +**Repository URL**: https://github.com/petgraph/petgraph **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -32450,7 +33697,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -32460,205 +33707,8 @@ limitations under the License. ``` -## pyo3-async-runtimes - 0.29.0 -**Repository URL**: https://github.com/PyO3/pyo3-async-runtimes -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -``` - -## pyo3-build-config - 0.29.0 -**Repository URL**: https://github.com/pyo3/pyo3 +## pin-project - 1.1.11 +**Repository URL**: https://github.com/taiki-e/pin-project **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -32738,8 +33788,8 @@ limitations under the License. ``` -## pyo3-ffi - 0.29.0 -**Repository URL**: https://github.com/pyo3/pyo3 +## pin-project-internal - 1.1.11 +**Repository URL**: https://github.com/taiki-e/pin-project **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -32819,8 +33869,8 @@ limitations under the License. ``` -## pyo3-macros - 0.29.0 -**Repository URL**: https://github.com/pyo3/pyo3 +## pin-project-lite - 0.2.17 +**Repository URL**: https://github.com/taiki-e/pin-project-lite **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -32900,34 +33950,243 @@ limitations under the License. ``` -## pyo3-macros-backend - 0.29.0 -**Repository URL**: https://github.com/pyo3/pyo3 +## pkg-config - 0.3.33 +**Repository URL**: https://github.com/rust-lang/pkg-config-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## portable-atomic - 1.13.1 +**Repository URL**: https://github.com/taiki-e/portable-atomic +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." @@ -32981,70 +34240,147 @@ limitations under the License. ``` -## pythonize - 0.29.0 -**Repository URL**: https://github.com/davidhewitt/pythonize -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## portable-atomic-util - 0.2.7 +**Repository URL**: https://github.com/taiki-e/portable-atomic-util +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -Copyright (c) 2022-present David Hewitt and Contributors +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## quick-xml - 0.39.4 -**Repository URL**: https://github.com/tafia/quick-xml -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## potential_utf - 0.1.5 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html ``` -The MIT License (MIT) - -Copyright (c) 2016 Johann Tuffe - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. ``` -## quinn - 0.11.11 -**Repository URL**: https://github.com/quinn-rs/quinn -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## powerfmt - 0.2.0 +**Repository URL**: https://github.com/jhpratt/powerfmt +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -33233,7 +34569,7 @@ THE SOFTWARE. same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2023 Jacob Pratt et al. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33246,461 +34582,4917 @@ THE SOFTWARE. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + ``` -### License File: LICENSE-MIT +## ppv-lite86 - 0.2.21 +**Repository URL**: https://github.com/cryptocorrosion/cryptocorrosion +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -Copyright (c) 2018 The quinn Developers + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -## quinn-proto - 0.11.15 -**Repository URL**: https://github.com/quinn-rs/quinn -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - 1. Definitions. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019 The CryptoCorrosion Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## prettyplease - 0.2.37 +**Repository URL**: https://github.com/dtolnay/prettyplease +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## primal-check - 0.3.4 +**Repository URL**: https://github.com/huonw/primal +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## proc-macro2 - 1.0.106 +**Repository URL**: https://github.com/dtolnay/proc-macro2 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## prost - 0.14.3 +**Repository URL**: https://github.com/tokio-rs/prost +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## prost-build - 0.14.3 +**Repository URL**: https://github.com/tokio-rs/prost +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## prost-derive - 0.14.3 +**Repository URL**: https://github.com/tokio-rs/prost +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## prost-types - 0.14.3 +**Repository URL**: https://github.com/tokio-rs/prost +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## protoc-bin-vendored - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-linux-aarch_64 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-linux-ppcle_64 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-linux-s390_64 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-linux-x86_32 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-linux-x86_64 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-macos-aarch_64 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-macos-x86_64 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## protoc-bin-vendored-win32 - 3.2.0 +**Repository URL**: https://github.com/stepancheg/rust-protoc-bin-vendored/ +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## pulldown-cmark - 0.13.4 +**Repository URL**: https://github.com/raphlinus/pulldown-cmark +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License + +Copyright 2015 Google Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + +## pulldown-cmark-to-cmark - 22.0.0 +**Repository URL**: https://github.com/Byron/pulldown-cmark-to-cmark +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 "Sebastian Thiel ", "Dylan Owen ", "Alessandro Ogier ", "Zixian Cai <2891235+caizixian@users.noreply.github.com>" + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +``` + +## pyo3 - 0.29.0 +**Repository URL**: https://github.com/pyo3/pyo3 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## pyo3-async-runtimes - 0.29.0 +**Repository URL**: https://github.com/PyO3/pyo3-async-runtimes +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +``` + +## pyo3-build-config - 0.29.0 +**Repository URL**: https://github.com/pyo3/pyo3 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## pyo3-ffi - 0.29.0 +**Repository URL**: https://github.com/pyo3/pyo3 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## pyo3-macros - 0.29.0 +**Repository URL**: https://github.com/pyo3/pyo3 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## pyo3-macros-backend - 0.29.0 +**Repository URL**: https://github.com/pyo3/pyo3 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## pythonize - 0.29.0 +**Repository URL**: https://github.com/davidhewitt/pythonize +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2022-present David Hewitt and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## quick-xml - 0.39.4 +**Repository URL**: https://github.com/tafia/quick-xml +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2016 Johann Tuffe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + +## quinn - 0.11.11 +**Repository URL**: https://github.com/quinn-rs/quinn +**License Type(s)**: MIT OR Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + +### License File: LICENSE-MIT +``` +Copyright (c) 2018 The quinn Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +## quinn-proto - 0.11.15 +**Repository URL**: https://github.com/quinn-rs/quinn +**License Type(s)**: MIT OR Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + +### License File: LICENSE-MIT +``` +Copyright (c) 2018 The quinn Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +## quinn-udp - 0.5.14 +**Repository URL**: https://github.com/quinn-rs/quinn +**License Type(s)**: MIT OR Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + +### License File: LICENSE-MIT +``` +Copyright (c) 2018 The quinn Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +## quote - 1.0.45 +**Repository URL**: https://github.com/dtolnay/quote +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## r-efi - 5.3.0 +**Repository URL**: https://github.com/r-efi/r-efi +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## r-efi - 6.0.0 +**Repository URL**: https://github.com/r-efi/r-efi +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## rand - 0.10.1 +**Repository URL**: https://github.com/rust-random/rand +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## rand - 0.9.3 +**Repository URL**: https://github.com/rust-random/rand +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## rand_chacha - 0.9.0 +**Repository URL**: https://github.com/rust-random/rand +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## rand_core - 0.10.1 +**Repository URL**: https://github.com/rust-random/rand_core +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +``` + +## rand_core - 0.9.5 +**Repository URL**: https://github.com/rust-random/rand +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +``` + +## rand_distr - 0.6.0 +**Repository URL**: https://github.com/rust-random/rand_distr +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +``` + +## rawpointer - 0.2.1 +**Repository URL**: https://github.com/bluss/rawpointer/ +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## rayon - 1.12.0 +**Repository URL**: https://github.com/rayon-rs/rayon +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - END OF TERMS AND CONDITIONS + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - APPENDIX: How to apply the Apache License to your work. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - Copyright [yyyy] [name of copyright owner] +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - http://www.apache.org/licenses/LICENSE-2.0 +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -``` +END OF TERMS AND CONDITIONS -### License File: LICENSE-MIT -``` -Copyright (c) 2018 The quinn Developers +APPENDIX: How to apply the Apache License to your work. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Copyright [yyyy] [name of copyright owner] -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -## quinn-udp - 0.5.14 -**Repository URL**: https://github.com/quinn-rs/quinn -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + http://www.apache.org/licenses/LICENSE-2.0 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. - 1. Definitions. +``` - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +## rayon-core - 1.13.0 +**Repository URL**: https://github.com/rayon-rs/rayon +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +1. Definitions. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - END OF TERMS AND CONDITIONS +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - APPENDIX: How to apply the Apache License to your work. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - Copyright [yyyy] [name of copyright owner] +END OF TERMS AND CONDITIONS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +APPENDIX: How to apply the Apache License to your work. - http://www.apache.org/licenses/LICENSE-2.0 + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -``` +Copyright [yyyy] [name of copyright owner] -### License File: LICENSE-MIT -``` -Copyright (c) 2018 The quinn Developers +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` - -## quote - 1.0.45 -**Repository URL**: https://github.com/dtolnay/quote + +## rcgen - 0.13.2 +**Repository URL**: https://github.com/rustls/rcgen **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -33780,8 +39572,79 @@ limitations under the License. ``` -## r-efi - 5.3.0 -**Repository URL**: https://github.com/r-efi/r-efi +## redis - 1.2.0 +**Repository URL**: https://github.com/redis-rs/redis-rs +**License Type(s)**: BSD-3-Clause +### License: https://spdx.org/licenses/BSD-3-Clause.html +``` +Copyright (c) 2022 by redis-rs contributors + +Redis cluster code in parts copyright (c) 2018 by Atsushi Koge. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +## redox_syscall - 0.5.18 +**Repository URL**: https://gitlab.redox-os.org/redox-os/syscall +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2017 Redox OS Developers + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## ref-cast - 1.0.25 +**Repository URL**: https://github.com/dtolnay/ref-cast **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -33861,8 +39724,8 @@ limitations under the License. ``` -## r-efi - 6.0.0 -**Repository URL**: https://github.com/r-efi/r-efi +## ref-cast-impl - 1.0.25 +**Repository URL**: https://github.com/dtolnay/ref-cast **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -33924,7 +39787,454 @@ END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## referencing - 0.46.8 +**Repository URL**: https://github.com/Stranger6667/jsonschema +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2020-2026 Dmitry Dygalo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## regex - 1.12.3 +**Repository URL**: https://github.com/rust-lang/regex +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## regex-automata - 0.4.14 +**Repository URL**: https://github.com/rust-lang/regex +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -33932,7 +40242,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -33942,70 +40252,198 @@ limitations under the License. ``` -## rand - 0.10.1 -**Repository URL**: https://github.com/rust-random/rand +## regex-syntax - 0.8.10 +**Repository URL**: https://github.com/rust-lang/regex **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -34013,7 +40451,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -34023,78 +40461,206 @@ limitations under the License. ``` -## rand - 0.9.3 -**Repository URL**: https://github.com/rust-random/rand +## reqwest - 0.12.28 +**Repository URL**: https://github.com/seanmonstar/reqwest **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright 2016 Sean McArthur Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -34104,70 +40670,198 @@ limitations under the License. ``` -## rand_chacha - 0.9.0 -**Repository URL**: https://github.com/rust-random/rand +## ring - 0.17.14 +**Repository URL**: https://github.com/briansmith/ring **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -34175,7 +40869,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -34185,14 +40879,36 @@ limitations under the License. ``` -## rand_core - 0.10.1 -**Repository URL**: https://github.com/rust-random/rand_core -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## ring - 0.17.14 +**Repository URL**: https://github.com/briansmith/ring +**License Type(s)**: ISC +### License: https://spdx.org/licenses/ISC.html +``` +Copyright 2015-2025 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + +## rustc-hash - 2.1.2 +**Repository URL**: https://github.com/rust-lang/rustc-hash +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -34366,28 +41082,43 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS +``` -APPENDIX: How to apply the Apache License to your work. +### License File: LICENSE-MIT +``` +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. ``` -## rand_core - 0.9.5 -**Repository URL**: https://github.com/rust-random/rand +## rustc_version - 0.4.1 +**Repository URL**: https://github.com/djc/rustc-version-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -34573,232 +41304,13 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -``` - -## rcgen - 0.13.2 -**Repository URL**: https://github.com/rustls/rcgen -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## redis - 1.2.0 -**Repository URL**: https://github.com/redis-rs/redis-rs -**License Type(s)**: BSD-3-Clause -### License: https://spdx.org/licenses/BSD-3-Clause.html -``` -Copyright (c) 2022 by redis-rs contributors - -Redis cluster code in parts copyright (c) 2018 by Atsushi Koge. - -Some rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -``` - -## redox_syscall - 0.5.18 -**Repository URL**: https://gitlab.redox-os.org/redox-os/syscall -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2017 Redox OS Developers - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## ref-cast - 1.0.25 -**Repository URL**: https://github.com/dtolnay/ref-cast -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -34808,118 +41320,218 @@ limitations under the License. ``` -## ref-cast-impl - 1.0.25 -**Repository URL**: https://github.com/dtolnay/ref-cast +## rustfft - 6.4.1 +**Repository URL**: https://github.com/ejmahler/RustFFT **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## referencing - 0.46.8 -**Repository URL**: https://github.com/Stranger6667/jsonschema -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2020-2026 Dmitry Dygalo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ``` -## regex - 1.12.3 -**Repository URL**: https://github.com/rust-lang/regex +## rustix - 1.1.4 +**Repository URL**: https://github.com/bytecodealliance/rustix **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -35127,8 +41739,8 @@ limitations under the License. ``` -## regex-automata - 0.4.14 -**Repository URL**: https://github.com/rust-lang/regex +## rustls - 0.23.40 +**Repository URL**: https://github.com/rustls/rustls **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -35336,8 +41948,8 @@ limitations under the License. ``` -## regex-syntax - 0.8.10 -**Repository URL**: https://github.com/rust-lang/regex +## rustls-native-certs - 0.8.3 +**Repository URL**: https://github.com/rustls/rustls-native-certs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -35545,8 +42157,8 @@ limitations under the License. ``` -## reqwest - 0.12.28 -**Repository URL**: https://github.com/seanmonstar/reqwest +## rustls-pki-types - 1.14.1 +**Repository URL**: https://github.com/rustls/pki-types **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -35729,22 +42341,292 @@ END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2023 Dirkjan Ochtman + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## rustls-webpki - 0.103.13 +**Repository URL**: https://github.com/rustls/webpki +**License Type(s)**: ISC +### License: https://spdx.org/licenses/ISC.html +``` +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + +``` + +## rustversion - 1.0.22 +**Repository URL**: https://github.com/dtolnay/rustversion +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## ryu - 1.0.23 +**Repository URL**: https://github.com/dtolnay/ryu +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## ryu-js - 1.0.2 +**Repository URL**: https://github.com/boa-dev/ryu-js +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2016 Sean McArthur +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -35754,241 +42636,351 @@ limitations under the License. ``` -## ring - 0.17.14 -**Repository URL**: https://github.com/briansmith/ring +## safetensors - 0.8.0 +**Repository URL**: https://github.com/huggingface/safetensors **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. + 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. + APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright [yyyy] [name of copyright owner] + Copyright [yyyy] [name of copyright owner] -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ``` -## ring - 0.17.14 -**Repository URL**: https://github.com/briansmith/ring -**License Type(s)**: ISC -### License: https://spdx.org/licenses/ISC.html +## same-file - 1.0.6 +**Repository URL**: https://github.com/BurntSushi/same-file +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Copyright 2015-2025 Brian Smith. +The MIT License (MIT) -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Copyright (c) 2017 Andrew Gallant -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ``` -## rustc-hash - 2.1.2 -**Repository URL**: https://github.com/rust-lang/rustc-hash -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## scan_fmt - 0.2.6 +**Repository URL**: https://github.com/wlentz/scan_fmt +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2015 wlentz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +``` + +## schannel - 0.1.29 +**Repository URL**: https://github.com/steffengy/schannel-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2015 steffengy + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## schemars - 0.8.22 +**Repository URL**: https://github.com/GREsau/schemars +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2019 Graham Esau + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## schemars_derive - 0.8.22 +**Repository URL**: https://github.com/GREsau/schemars +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2019 Graham Esau + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## scopeguard - 1.2.0 +**Repository URL**: https://github.com/bluss/scopeguard +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -36166,37 +43158,36 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS -``` -### License File: LICENSE-MIT -``` -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ``` -## rustc_version - 0.4.1 -**Repository URL**: https://github.com/djc/rustc-version-rs +## security-framework - 3.7.0 +**Repository URL**: https://github.com/kornelski/rust-security-framework **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -36404,8 +43395,8 @@ limitations under the License. ``` -## rustix - 1.1.4 -**Repository URL**: https://github.com/bytecodealliance/rustix +## security-framework-sys - 2.17.0 +**Repository URL**: https://github.com/kornelski/rust-security-framework **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -36613,10 +43604,173 @@ limitations under the License. ``` -## rustls - 0.23.40 -**Repository URL**: https://github.com/rustls/rustls +## semver - 1.0.28 +**Repository URL**: https://github.com/dtolnay/semver +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## serde - 1.0.228 +**Repository URL**: https://github.com/serde-rs/serde **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## serde_buf - 0.1.2 +**Repository URL**: https://github.com/KodrAus/serde_buf.git +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -36740,71 +43894,338 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION that such additional attribution notices cannot be construed as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +### License File: LICENSE-MIT +``` +MIT License + +Copyright (c) 2019 Ashley Mannix + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## serde_core - 1.0.228 +**Repository URL**: https://github.com/serde-rs/serde +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## serde_derive - 1.0.228 +**Repository URL**: https://github.com/serde-rs/serde +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## serde_derive_internals - 0.29.1 +**Repository URL**: https://github.com/serde-rs/serde +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -36812,7 +44233,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -36822,10 +44243,11 @@ limitations under the License. ``` -## rustls-native-certs - 0.8.3 -**Repository URL**: https://github.com/rustls/rustls-native-certs -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## serde_fmt - 1.1.0 +**Repository URL**: https://github.com/KodrAus/serde_fmt.git +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -37028,209 +44450,105 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +``` +### License File: LICENSE-MIT ``` +MIT License -## rustls-pki-types - 1.14.1 -**Repository URL**: https://github.com/rustls/pki-types +Copyright (c) 2019 Ashley Mannix + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## serde_json - 1.0.149 +**Repository URL**: https://github.com/serde-rs/json **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2023 Dirkjan Ochtman +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -37240,35 +44558,37 @@ limitations under the License. ``` -## rustls-webpki - 0.103.13 -**Repository URL**: https://github.com/rustls/webpki -**License Type(s)**: ISC -### License: https://spdx.org/licenses/ISC.html +## serde_json_canonicalizer - 0.3.2 +**Repository URL**: https://github.com/evik42/serde-json-canonicalizer +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Except as otherwise noted, this project is licensed under the following -(ISC-style) terms: - -Copyright 2015 Brian Smith. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -The files under third-party/chromium are licensed as described in -third-party/chromium/LICENSE. +MIT License + +Copyright (c) 2023 Attila Mravik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## rustversion - 1.0.22 -**Repository URL**: https://github.com/dtolnay/rustversion +## serde_path_to_error - 0.1.20 +**Repository URL**: https://github.com/dtolnay/path-to-error **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -37312,44 +44632,254 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## serde_spanned - 1.1.1 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + END OF TERMS AND CONDITIONS -END OF TERMS AND CONDITIONS + APPENDIX: How to apply the Apache License to your work. -APPENDIX: How to apply the Apache License to your work. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + Copyright {yyyy} {name of copyright owner} -Copyright [yyyy] [name of copyright owner] + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 -http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ``` -## ryu - 1.0.23 -**Repository URL**: https://github.com/dtolnay/ryu +## serde_urlencoded - 0.7.1 +**Repository URL**: https://github.com/nox/serde_urlencoded **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -37429,8 +44959,8 @@ limitations under the License. ``` -## ryu-js - 1.0.2 -**Repository URL**: https://github.com/boa-dev/ryu-js +## serde_yaml - 0.9.34+deprecated +**Repository URL**: https://github.com/dtolnay/serde-yaml **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -37510,81 +45040,8 @@ limitations under the License. ``` -## schannel - 0.1.29 -**Repository URL**: https://github.com/steffengy/schannel-rs -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2015 steffengy - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## schemars - 0.8.22 -**Repository URL**: https://github.com/GREsau/schemars -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2019 Graham Esau - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## schemars_derive - 0.8.22 -**Repository URL**: https://github.com/GREsau/schemars -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2019 Graham Esau - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## scopeguard - 1.2.0 -**Repository URL**: https://github.com/bluss/scopeguard +## sha1 - 0.10.6 +**Repository URL**: https://github.com/RustCrypto/hashes **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -37782,7 +45239,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -37792,8 +45249,27 @@ limitations under the License. ``` -## security-framework - 3.7.0 -**Repository URL**: https://github.com/kornelski/rust-security-framework +## sha1_smol - 1.0.1 +**Repository URL**: https://github.com/mitsuhiko/sha1-smol +**License Type(s)**: BSD-3-Clause +### License: https://spdx.org/licenses/BSD-3-Clause.html +``` +Copyright (c) . + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +## sha2 - 0.11.0 +**Repository URL**: https://github.com/RustCrypto/hashes **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -37949,41 +45425,331 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## shell-words - 1.1.1 +**Repository URL**: https://github.com/tmiasko/shell-words +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## shlex - 1.3.0 +**Repository URL**: https://github.com/comex/rust-shlex +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -37991,7 +45757,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -38001,8 +45767,8 @@ limitations under the License. ``` -## security-framework-sys - 2.17.0 -**Repository URL**: https://github.com/kornelski/rust-security-framework +## signal-hook-registry - 1.4.8 +**Repository URL**: https://github.com/vorner/signal-hook **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -38210,173 +45976,72 @@ limitations under the License. ``` -## semver - 1.0.28 -**Repository URL**: https://github.com/dtolnay/semver -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## simd-adler32 - 0.3.10 +**Repository URL**: https://github.com/mcountryman/simd-adler32 +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. +MIT License -Copyright [yyyy] [name of copyright owner] +Copyright (c) [2021] [Marvin Countryman] -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## serde - 1.0.228 -**Repository URL**: https://github.com/serde-rs/serde -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## slab - 0.4.12 +**Repository URL**: https://github.com/tokio-rs/slab +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2019 Carl Lerche -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: -http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. ``` -## serde_buf - 0.1.2 -**Repository URL**: https://github.com/KodrAus/serde_buf.git -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## smallvec - 1.15.1 +**Repository URL**: https://github.com/servo/rust-smallvec +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -38574,192 +46239,6 @@ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -``` - -### License File: LICENSE-MIT -``` -MIT License - -Copyright (c) 2019 Ashley Mannix - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## serde_core - 1.0.228 -**Repository URL**: https://github.com/serde-rs/serde -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## serde_derive - 1.0.228 -**Repository URL**: https://github.com/serde-rs/serde -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -38768,92 +46247,10 @@ limitations under the License. ``` -## serde_derive_internals - 0.29.1 -**Repository URL**: https://github.com/serde-rs/serde +## socket2 - 0.6.3 +**Repository URL**: https://github.com/rust-lang/socket2 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## serde_fmt - 1.1.0 -**Repository URL**: https://github.com/KodrAus/serde_fmt.git -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -39056,35 +46453,11 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` - -### License File: LICENSE-MIT -``` -MIT License - -Copyright (c) 2019 Ashley Mannix -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ``` -## serde_json - 1.0.149 -**Repository URL**: https://github.com/serde-rs/json +## spdlog-internal - 0.2.1 +**Repository URL**: https://github.com/SpriteOvO/spdlog-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -39164,37 +46537,8 @@ limitations under the License. ``` -## serde_json_canonicalizer - 0.3.2 -**Repository URL**: https://github.com/evik42/serde-json-canonicalizer -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) 2023 Attila Mravik - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## serde_path_to_error - 0.1.20 -**Repository URL**: https://github.com/dtolnay/path-to-error +## spdlog-macros - 0.3.1 +**Repository URL**: https://github.com/SpriteOvO/spdlog-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -39274,12 +46618,12 @@ limitations under the License. ``` -## serde_spanned - 1.1.1 -**Repository URL**: https://github.com/toml-rs/toml +## spdlog-rs - 0.5.3 +**Repository URL**: https://github.com/SpriteOvO/spdlog-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License +Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -39459,7 +46803,7 @@ limitations under the License. APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -39467,7 +46811,7 @@ limitations under the License. same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -39481,173 +46825,10 @@ limitations under the License. See the License for the specific language governing permissions and limitations under the License. - -``` - -## serde_urlencoded - 0.7.1 -**Repository URL**: https://github.com/nox/serde_urlencoded -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## serde_yaml - 0.9.34+deprecated -**Repository URL**: https://github.com/dtolnay/serde-yaml -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ``` -## sha1 - 0.10.6 -**Repository URL**: https://github.com/RustCrypto/hashes +## stable_deref_trait - 1.2.1 +**Repository URL**: https://github.com/storyyeller/stable_deref_trait **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -39815,29 +46996,320 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## strength_reduce - 0.2.4 +**Repository URL**: http://github.com/ejmahler/strength_reduce +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +``` + +## string-interner - 0.20.0 +**Repository URL**: https://github.com/robbepop/string-interner +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -39845,7 +47317,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -39855,29 +47327,137 @@ limitations under the License. ``` -## sha1_smol - 1.0.1 -**Repository URL**: https://github.com/mitsuhiko/sha1-smol +## strsim - 0.11.1 +**Repository URL**: https://github.com/rapidfuzz/strsim-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2015 Danny Guo +Copyright (c) 2016 Titus Wormer +Copyright (c) 2018 Akash Kurdekar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## strum - 0.27.2 +**Repository URL**: https://github.com/Peternator7/strum +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2019 Peter Glotfelty + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## strum_macros - 0.27.2 +**Repository URL**: https://github.com/Peternator7/strum +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2019 Peter Glotfelty + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## subtle - 2.6.1 +**Repository URL**: https://github.com/dalek-cryptography/subtle **License Type(s)**: BSD-3-Clause ### License: https://spdx.org/licenses/BSD-3-Clause.html ``` -Copyright (c) . +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` -## sha2 - 0.11.0 -**Repository URL**: https://github.com/RustCrypto/hashes -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## sval - 2.20.0 +**Repository URL**: https://github.com/sval-rs/sval +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -40073,310 +47653,45 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## shell-words - 1.1.1 -**Repository URL**: https://github.com/tmiasko/shell-words -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - ``` -## shlex - 1.3.0 -**Repository URL**: https://github.com/comex/rust-shlex -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +### License File: LICENSE-MIT ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2020 sval-rs -http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## signal-hook-registry - 1.4.8 -**Repository URL**: https://github.com/vorner/signal-hook -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## sval_buffer - 2.20.0 +**Repository URL**: https://github.com/sval-rs/sval +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -40579,46 +47894,38 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - ``` -## slab - 0.4.12 -**Repository URL**: https://github.com/tokio-rs/slab -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +### License File: LICENSE-MIT ``` -Copyright (c) 2019 Carl Lerche - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +MIT License -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +Copyright (c) 2020 sval-rs -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## smallvec - 1.15.1 -**Repository URL**: https://github.com/servo/rust-smallvec -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## sval_dynamic - 2.20.0 +**Repository URL**: https://github.com/sval-rs/sval +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -40821,13 +48128,38 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +``` +### License File: LICENSE-MIT ``` +MIT License -## socket2 - 0.6.3 -**Repository URL**: https://github.com/rust-lang/socket2 -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +Copyright (c) 2020 sval-rs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## sval_fmt - 2.20.0 +**Repository URL**: https://github.com/sval-rs/sval +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -41030,384 +48362,38 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -``` - -## spdlog-internal - 0.2.1 -**Repository URL**: https://github.com/SpriteOvO/spdlog-rs -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## spdlog-macros - 0.3.1 -**Repository URL**: https://github.com/SpriteOvO/spdlog-rs -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ``` -## spdlog-rs - 0.5.3 -**Repository URL**: https://github.com/SpriteOvO/spdlog-rs -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +### License File: LICENSE-MIT ``` -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] +MIT License - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Copyright (c) 2020 sval-rs - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## stable_deref_trait - 1.2.1 -**Repository URL**: https://github.com/storyyeller/stable_deref_trait -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## sval_json - 2.20.0 +**Repository URL**: https://github.com/sval-rs/sval +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -41573,114 +48559,50 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## strsim - 0.11.1 -**Repository URL**: https://github.com/rapidfuzz/strsim-rs -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -The MIT License (MIT) - -Copyright (c) 2015 Danny Guo -Copyright (c) 2016 Titus Wormer -Copyright (c) 2018 Akash Kurdekar - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -``` +END OF TERMS AND CONDITIONS -## strum - 0.27.2 -**Repository URL**: https://github.com/Peternator7/strum -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License +APPENDIX: How to apply the Apache License to your work. -Copyright (c) 2019 Peter Glotfelty + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Copyright [yyyy] [name of copyright owner] -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## strum_macros - 0.27.2 -**Repository URL**: https://github.com/Peternator7/strum -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +### License File: LICENSE-MIT ``` MIT License -Copyright (c) 2019 Peter Glotfelty +Copyright (c) 2020 sval-rs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -41699,47 +48621,23 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ``` -## subtle - 2.6.1 -**Repository URL**: https://github.com/dalek-cryptography/subtle -**License Type(s)**: BSD-3-Clause -### License: https://spdx.org/licenses/BSD-3-Clause.html +## sval_nested - 2.20.0 +**Repository URL**: https://github.com/sval-rs/sval +**License Type(s)**: Apache-2.0 OR MIT +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE +``` +../LICENSE-APACHE ``` -Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. -Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +### License File: LICENSE-MIT +``` +../LICENSE-MIT ``` -## sval - 2.20.0 +## sval_ref - 2.20.0 **Repository URL**: https://github.com/sval-rs/sval **License Type(s)**: Apache-2.0 OR MIT ### License: https://spdx.org/licenses/ @@ -41973,7 +48871,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## sval_buffer - 2.20.0 +## sval_serde - 2.20.0 **Repository URL**: https://github.com/sval-rs/sval **License Type(s)**: Apache-2.0 OR MIT ### License: https://spdx.org/licenses/ @@ -42121,285 +49019,580 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +### License File: LICENSE-MIT +``` +MIT License + +Copyright (c) 2020 sval-rs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## switchyard-protocol - 0.1.0 +**Repository URL**: https://github.com/NVIDIA-NeMo/Switchyard +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +``` + +## switchyard-translation - 0.1.0 +**Repository URL**: https://github.com/NVIDIA-NeMo/Switchyard +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -END OF TERMS AND CONDITIONS + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -APPENDIX: How to apply the Apache License to your work. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -Copyright [yyyy] [name of copyright owner] + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - http://www.apache.org/licenses/LICENSE-2.0 + END OF TERMS AND CONDITIONS -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -``` + APPENDIX: How to apply the Apache License to your work. -### License File: LICENSE-MIT -``` -MIT License + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright (c) 2020 sval-rs + Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ``` -## sval_dynamic - 2.20.0 -**Repository URL**: https://github.com/sval-rs/sval -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## syn - 2.0.117 +**Repository URL**: https://github.com/dtolnay/syn +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -42407,233 +49600,80 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` - -### License File: LICENSE-MIT -``` -MIT License - -Copyright (c) 2020 sval-rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ``` -## sval_fmt - 2.20.0 -**Repository URL**: https://github.com/sval-rs/sval -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## sync_wrapper - 1.0.2 +**Repository URL**: https://github.com/Actyx/sync_wrapper +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -42641,45 +49681,35 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + ``` -### License File: LICENSE-MIT +## synstructure - 0.13.2 +**Repository URL**: https://github.com/mystor/synstructure +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -MIT License +Copyright 2016 Nika Layzell -Copyright (c) 2020 sval-rs +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ``` -## sval_json - 2.20.0 -**Repository URL**: https://github.com/sval-rs/sval -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## tar - 0.4.46 +**Repository URL**: https://github.com/composefs/tar-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -42882,240 +49912,301 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + ``` -### License File: LICENSE-MIT +## target-lexicon - 0.13.5 +**Repository URL**: https://github.com/bytecodealliance/target-lexicon +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -MIT License -Copyright (c) 2020 sval-rs + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + 1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. -## sval_nested - 2.20.0 -**Repository URL**: https://github.com/sval-rs/sval -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE -``` -../LICENSE-APACHE -``` -### License File: LICENSE-MIT -``` -../LICENSE-MIT ``` -## sval_ref - 2.20.0 -**Repository URL**: https://github.com/sval-rs/sval -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## tdigest - 0.2.3 +**Repository URL**: https://github.com/MnO2/t-digest +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -43123,45 +50214,20 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` - -### License File: LICENSE-MIT -``` -MIT License - -Copyright (c) 2020 sval-rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ``` -## sval_serde - 2.20.0 -**Repository URL**: https://github.com/sval-rs/sval -**License Type(s)**: Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## tempfile - 3.27.0 +**Repository URL**: https://github.com/Stebalien/tempfile +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -43364,252 +50430,554 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + ``` -### License File: LICENSE-MIT +## thiserror - 1.0.69 +**Repository URL**: https://github.com/dtolnay/thiserror +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -MIT License +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -Copyright (c) 2020 sval-rs +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +1. Definitions. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ``` -## switchyard-protocol - 0.1.0 -**Repository URL**: https://github.com/NVIDIA-NeMo/Switchyard +## thiserror - 2.0.18 +**Repository URL**: https://github.com/dtolnay/thiserror **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +1. Definitions. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - 1. Definitions. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## thiserror-impl - 1.0.69 +**Repository URL**: https://github.com/dtolnay/thiserror +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## thiserror-impl - 2.0.18 +**Repository URL**: https://github.com/dtolnay/thiserror +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## time - 0.3.53 +**Repository URL**: https://github.com/time-rs/time +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## time-core - 0.1.9 +**Repository URL**: https://github.com/time-rs/time +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +END OF TERMS AND CONDITIONS - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +APPENDIX: How to apply the Apache License to your work. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +Copyright [yyyy] [name of copyright owner] - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +http://www.apache.org/licenses/LICENSE-2.0 - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +``` - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +## tinystr - 0.8.3 +**Repository URL**: https://github.com/unicode-org/icu4x +**License Type(s)**: Unicode-3.0 +### License: https://spdx.org/licenses/Unicode-3.0.html +``` +UNICODE LICENSE V3 - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +COPYRIGHT AND PERMISSION NOTICE - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +Copyright © 2020-2024 Unicode, Inc. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - END OF TERMS AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. - APPENDIX: How to apply the Apache License to your work. +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. - Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +SPDX-License-Identifier: Unicode-3.0 - http://www.apache.org/licenses/LICENSE-2.0 +— - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. ``` -## switchyard-translation - 0.1.0 -**Repository URL**: https://github.com/NVIDIA-NeMo/Switchyard +## tinyvec - 1.11.0 +**Repository URL**: https://github.com/Lokathor/tinyvec **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - Apache License Version 2.0, January 2004 @@ -43799,7 +51167,7 @@ Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -43815,189 +51183,11 @@ Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. ``` -## syn - 2.0.117 -**Repository URL**: https://github.com/dtolnay/syn -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## sync_wrapper - 1.0.2 -**Repository URL**: https://github.com/Actyx/sync_wrapper -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -``` - -## synstructure - 0.13.2 -**Repository URL**: https://github.com/mystor/synstructure -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright 2016 Nika Layzell - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## target-lexicon - 0.13.5 -**Repository URL**: https://github.com/bytecodealliance/target-lexicon +## tinyvec_macros - 0.1.1 +**Repository URL**: https://github.com/Soveu/tinyvec_macros **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -44186,7 +51376,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 Tomasz "Soveu" Marx Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -44201,108 +51391,69 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI limitations under the License. ---- LLVM Exceptions to the Apache 2.0 License ---- - -As an exception, if, as a result of your compiling your source code, portions -of this Software are embedded into an Object form of such source code, you -may redistribute such embedded portions in such Object form without complying -with the conditions of Sections 4(a), 4(b) and 4(d) of the License. - -In addition, if you combine or link compiled forms of this Software with -software that is licensed under the GPLv2 ("Combined Software") and if a -court of competent jurisdiction determines that the patent provision (Section -3), the indemnity provision (Section 9) or other Section of the License -conflicts with the conditions of the GPLv2, you may retroactively and -prospectively choose to deem waived or otherwise exclude such Section(s) of -the License, but only in their entirety and only with respect to the Combined -Software. - - ``` -## tdigest - 0.2.3 -**Repository URL**: https://github.com/MnO2/t-digest -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## tokio - 1.51.1 +**Repository URL**: https://github.com/tokio-rs/tokio +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +MIT License -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +Copyright (c) Tokio Contributors -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -END OF TERMS AND CONDITIONS +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -APPENDIX: How to apply the Apache License to your work. +``` -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. +## tokio-macros - 2.7.0 +**Repository URL**: https://github.com/tokio-rs/tokio +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2019 Yoshua Wuyts +Copyright (c) Tokio Contributors -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## tempfile - 3.27.0 -**Repository URL**: https://github.com/Stebalien/tempfile +## tokio-rustls - 0.26.4 +**Repository URL**: https://github.com/rustls/tokio-rustls **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -44494,7 +51645,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright 2017 quininer kel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -44510,553 +51661,727 @@ limitations under the License. ``` -## thiserror - 1.0.69 -**Repository URL**: https://github.com/dtolnay/thiserror -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## tokio-stream - 0.1.18 +**Repository URL**: https://github.com/tokio-rs/tokio +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. +MIT License -Copyright [yyyy] [name of copyright owner] +Copyright (c) Tokio Contributors -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## thiserror - 2.0.18 -**Repository URL**: https://github.com/dtolnay/thiserror -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## tokio-tungstenite - 0.27.0 +**Repository URL**: https://github.com/snapview/tokio-tungstenite +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] +Copyright (c) 2017 Daniel Abramov +Copyright (c) 2017 Alexey Galakhov -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ``` -## thiserror-impl - 1.0.69 -**Repository URL**: https://github.com/dtolnay/thiserror -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +## tokio-util - 0.7.18 +**Repository URL**: https://github.com/tokio-rs/tokio +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. +MIT License -Copyright [yyyy] [name of copyright owner] +Copyright (c) Tokio Contributors -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` -## thiserror-impl - 2.0.18 -**Repository URL**: https://github.com/dtolnay/thiserror +## toml - 0.9.12+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. + 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + Copyright {yyyy} {name of copyright owner} -Copyright [yyyy] [name of copyright owner] + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 -http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ``` -## time - 0.3.53 -**Repository URL**: https://github.com/time-rs/time +## toml_datetime - 0.7.5+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. + 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. + APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright [yyyy] [name of copyright owner] + Copyright {yyyy} {name of copyright owner} -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ``` -## time-core - 0.1.9 -**Repository URL**: https://github.com/time-rs/time +## toml_edit - 0.23.10+spec-1.0.0 +**Repository URL**: https://github.com/toml-rs/toml **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 1. Definitions. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -END OF TERMS AND CONDITIONS + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -APPENDIX: How to apply the Apache License to your work. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -Copyright [yyyy] [name of copyright owner] + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and -http://www.apache.org/licenses/LICENSE-2.0 + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -``` + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -## tinystr - 0.8.3 -**Repository URL**: https://github.com/unicode-org/icu4x -**License Type(s)**: Unicode-3.0 -### License: https://spdx.org/licenses/Unicode-3.0.html -``` -UNICODE LICENSE V3 + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -COPYRIGHT AND PERMISSION NOTICE + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -Copyright © 2020-2024 Unicode, Inc. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. + END OF TERMS AND CONDITIONS -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -THIRD PARTY RIGHTS. + APPENDIX: How to apply the Apache License to your work. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. + Copyright {yyyy} {name of copyright owner} -SPDX-License-Identifier: Unicode-3.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -— + http://www.apache.org/licenses/LICENSE-2.0 -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -``` -## tinyvec - 1.11.0 -**Repository URL**: https://github.com/Lokathor/tinyvec -**License Type(s)**: Zlib OR Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE.md ``` +## toml_parser - 1.1.2+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -45237,7 +52562,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -45245,7 +52570,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45258,37 +52583,14 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` - -### License File: LICENSE-MIT.md -``` -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` - -### License File: LICENSE-ZLIB.md -``` -Copyright (c) 2019 Daniel "Lokathor" Gee. -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. ``` -## tinyvec_macros - 0.1.1 -**Repository URL**: https://github.com/Soveu/tinyvec_macros -**License Type(s)**: MIT OR Apache-2.0 OR Zlib -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE.md +## toml_writer - 1.1.1+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -45470,7 +52772,7 @@ Permission is granted to anyone to use this software for any purpose, including APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -45478,7 +52780,7 @@ Permission is granted to anyone to use this software for any purpose, including same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2020 Tomasz "Soveu" Marx + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45492,13 +52794,42 @@ Permission is granted to anyone to use this software for any purpose, including See the License for the specific language governing permissions and limitations under the License. + ``` -### License File: LICENSE-MIT.md +## tonic - 0.14.5 +**Repository URL**: https://github.com/hyperium/tonic +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2025 Lucio Franco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ``` -MIT License -Copyright (c) 2020 Soveu +## tonic-build - 0.14.6 +**Repository URL**: https://github.com/hyperium/tonic +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2025 Lucio Franco Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -45507,103 +52838,513 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ``` -### License File: LICENSE-ZLIB.md +## tonic-prost - 0.14.5 +**Repository URL**: https://github.com/hyperium/tonic +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html ``` -zlib License +MIT License -(C) 2020 Tomasz "Soveu" Marx +Copyright (c) -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## tokio - 1.51.1 -**Repository URL**: https://github.com/tokio-rs/tokio +## tonic-prost-build - 0.14.6 +**Repository URL**: https://github.com/hyperium/tonic **License Type(s)**: MIT ### License: https://spdx.org/licenses/MIT.html ``` MIT License -Copyright (c) Tokio Contributors +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## tower - 0.5.3 +**Repository URL**: https://github.com/tower-rs/tower +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019 Tower Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## tower-http - 0.6.8 +**Repository URL**: https://github.com/tower-rs/tower-http +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019-2021 Tower Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## tower-layer - 0.3.3 +**Repository URL**: https://github.com/tower-rs/tower +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019 Tower Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## tower-service - 0.3.3 +**Repository URL**: https://github.com/tower-rs/tower +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019 Tower Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## tracing - 0.1.44 +**Repository URL**: https://github.com/tokio-rs/tracing +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019 Tokio Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## tracing-attributes - 0.1.31 +**Repository URL**: https://github.com/tokio-rs/tracing +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019 Tokio Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## tracing-core - 0.1.36 +**Repository URL**: https://github.com/tokio-rs/tracing +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019 Tokio Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + +## tract-core - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +END OF TERMS AND CONDITIONS -``` +APPENDIX: How to apply the Apache License to your work. -## tokio-macros - 2.7.0 -**Repository URL**: https://github.com/tokio-rs/tokio -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright (c) 2019 Yoshua Wuyts -Copyright (c) Tokio Contributors +Copyright [yyyy] [name of copyright owner] -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + http://www.apache.org/licenses/LICENSE-2.0 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## tokio-rustls - 0.26.4 -**Repository URL**: https://github.com/rustls/tokio-rustls +## tract-data - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -45795,7 +53536,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2017 quininer kel +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45811,1510 +53552,1343 @@ limitations under the License. ``` -## tokio-stream - 0.1.18 -**Repository URL**: https://github.com/tokio-rs/tokio -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) Tokio Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## tokio-tungstenite - 0.27.0 -**Repository URL**: https://github.com/snapview/tokio-tungstenite -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2017 Daniel Abramov -Copyright (c) 2017 Alexey Galakhov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -``` - -## tokio-util - 0.7.18 -**Repository URL**: https://github.com/tokio-rs/tokio -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License - -Copyright (c) Tokio Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -``` - -## toml - 0.9.12+spec-1.1.0 -**Repository URL**: https://github.com/toml-rs/toml +## tract-extra - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ - 1. Definitions. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +1. Definitions. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - END OF TERMS AND CONDITIONS +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - APPENDIX: How to apply the Apache License to your work. +END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +APPENDIX: How to apply the Apache License to your work. - Copyright {yyyy} {name of copyright owner} +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Copyright [yyyy] [name of copyright owner] - http://www.apache.org/licenses/LICENSE-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## toml_datetime - 0.7.5+spec-1.1.0 -**Repository URL**: https://github.com/toml-rs/toml +## tract-hir - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +1. Definitions. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - END OF TERMS AND CONDITIONS +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - APPENDIX: How to apply the Apache License to your work. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +END OF TERMS AND CONDITIONS - Copyright {yyyy} {name of copyright owner} +APPENDIX: How to apply the Apache License to your work. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - http://www.apache.org/licenses/LICENSE-2.0 +Copyright [yyyy] [name of copyright owner] - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## toml_edit - 0.23.10+spec-1.0.0 -**Repository URL**: https://github.com/toml-rs/toml +## tract-linalg - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - 1. Definitions. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +1. Definitions. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - END OF TERMS AND CONDITIONS +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - APPENDIX: How to apply the Apache License to your work. +END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +APPENDIX: How to apply the Apache License to your work. - Copyright {yyyy} {name of copyright owner} + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Copyright [yyyy] [name of copyright owner] - http://www.apache.org/licenses/LICENSE-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## toml_parser - 1.1.2+spec-1.1.0 -**Repository URL**: https://github.com/toml-rs/toml +## tract-nnef - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - 1. Definitions. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +1. Definitions. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - END OF TERMS AND CONDITIONS +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - APPENDIX: How to apply the Apache License to your work. +END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +APPENDIX: How to apply the Apache License to your work. - Copyright {yyyy} {name of copyright owner} + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Copyright [yyyy] [name of copyright owner] - http://www.apache.org/licenses/LICENSE-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## toml_writer - 1.1.1+spec-1.1.0 -**Repository URL**: https://github.com/toml-rs/toml +## tract-onnx - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +1. Definitions. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - END OF TERMS AND CONDITIONS +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - APPENDIX: How to apply the Apache License to your work. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +END OF TERMS AND CONDITIONS - Copyright {yyyy} {name of copyright owner} +APPENDIX: How to apply the Apache License to your work. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - http://www.apache.org/licenses/LICENSE-2.0 +Copyright [yyyy] [name of copyright owner] - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## tonic - 0.14.5 -**Repository URL**: https://github.com/hyperium/tonic -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## tract-onnx-opl - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -Copyright (c) 2025 Lucio Franco + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -``` + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -## tonic-build - 0.14.6 -**Repository URL**: https://github.com/hyperium/tonic -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2025 Lucio Franco + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -``` + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -## tonic-prost - 0.14.5 -**Repository URL**: https://github.com/hyperium/tonic -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -Copyright (c) + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -``` +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -## tonic-prost-build - 0.14.6 -**Repository URL**: https://github.com/hyperium/tonic -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -MIT License + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -Copyright (c) + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -``` +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -## tower - 0.5.3 -**Repository URL**: https://github.com/tower-rs/tower -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019 Tower Contributors +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -``` +END OF TERMS AND CONDITIONS -## tower-http - 0.6.8 -**Repository URL**: https://github.com/tower-rs/tower-http -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019-2021 Tower Contributors +APPENDIX: How to apply the Apache License to your work. -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## tower-layer - 0.3.3 -**Repository URL**: https://github.com/tower-rs/tower -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html +## tract-pulse - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -Copyright (c) 2019 Tower Contributors + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -``` + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -## tower-service - 0.3.3 -**Repository URL**: https://github.com/tower-rs/tower -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019 Tower Contributors + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -``` + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -## tracing - 0.1.44 -**Repository URL**: https://github.com/tokio-rs/tracing -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019 Tokio Contributors + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -``` +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -## tracing-attributes - 0.1.31 -**Repository URL**: https://github.com/tokio-rs/tracing -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019 Tokio Contributors +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and -``` + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -## tracing-core - 0.1.36 -**Repository URL**: https://github.com/tokio-rs/tracing -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2019 Tokio Contributors + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -``` +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -## try-lock - 0.2.5 -**Repository URL**: https://github.com/seanmonstar/try-lock -**License Type(s)**: MIT -### License: https://spdx.org/licenses/MIT.html -``` -Copyright (c) 2018-2023 Sean McArthur -Copyright (c) 2016 Alex Crichton +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +END OF TERMS AND CONDITIONS -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +APPENDIX: How to apply the Apache License to your work. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` -## tungstenite - 0.27.0 -**Repository URL**: https://github.com/snapview/tungstenite-rs +## tract-pulse-opl - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -47522,8 +55096,327 @@ limitations under the License. ``` -## typed-builder - 0.23.2 -**Repository URL**: https://github.com/idanarye/rust-typed-builder +## tract-transformers - 0.23.4 +**Repository URL**: https://github.com/snipsco/tract +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## transpose - 0.2.3 +**Repository URL**: https://github.com/ejmahler/transpose +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2022 The transpose developers + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + +## try-lock - 0.2.5 +**Repository URL**: https://github.com/seanmonstar/try-lock +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2018-2023 Sean McArthur +Copyright (c) 2016 Alex Crichton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +``` + +## tungstenite - 0.27.0 +**Repository URL**: https://github.com/snapview/tungstenite-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -47731,7 +55624,7 @@ limitations under the License. ``` -## typed-builder-macro - 0.23.2 +## typed-builder - 0.23.2 **Repository URL**: https://github.com/idanarye/rust-typed-builder **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html @@ -47940,11 +55833,10 @@ limitations under the License. ``` -## typeid - 1.0.3 -**Repository URL**: https://github.com/dtolnay/typeid -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +## typed-builder-macro - 0.23.2 +**Repository URL**: https://github.com/idanarye/rust-typed-builder +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -48122,33 +56014,113 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + ``` -### License File: LICENSE-MIT +## typeid - 1.0.3 +**Repository URL**: https://github.com/dtolnay/typeid +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ``` ## typenum - 1.19.0 @@ -48905,6 +56877,215 @@ authorization of the copyright holder. ``` +## unicode-normalization - 0.1.25 +**Repository URL**: https://github.com/unicode-rs/unicode-normalization +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + ## unicode-segmentation - 1.13.2 **Repository URL**: https://github.com/unicode-rs/unicode-segmentation **License Type(s)**: Apache-2.0 @@ -49561,6 +57742,216 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +## unicode_categories - 0.1.1 +**Repository URL**: https://github.com/swgillespie/unicode-categories +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +``` + ## unsafe-libyaml - 0.2.11 **Repository URL**: https://github.com/dtolnay/unsafe-libyaml **License Type(s)**: MIT @@ -51341,6 +59732,35 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +## walkdir - 2.5.0 +**Repository URL**: https://github.com/BurntSushi/walkdir +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + ## want - 0.3.1 **Repository URL**: https://github.com/seanmonstar/want **License Type(s)**: MIT @@ -53601,6 +62021,35 @@ limitations under the License. ``` +## winapi-util - 0.1.11 +**Repository URL**: https://github.com/BurntSushi/winapi-util +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2017 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + ## winapi-x86_64-pc-windows-gnu - 0.4.0 **Repository URL**: https://github.com/retep998/winapi-rs **License Type(s)**: Apache-2.0 @@ -59157,6 +67606,215 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a ``` +## xattr - 1.6.1 +**Repository URL**: https://github.com/Stebalien/xattr +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + ## xxhash-rust - 0.8.15 **Repository URL**: https://github.com/DoumanAsh/xxhash-rust **License Type(s)**: BSL-1.0 @@ -59589,9 +68247,8 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a ## zerocopy-derive - 0.8.48 **Repository URL**: https://github.com/google/zerocopy -**License Type(s)**: BSD-2-Clause OR Apache-2.0 OR MIT -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -59795,63 +68452,6 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a See the License for the specific language governing permissions and limitations under the License. -``` - -### License File: LICENSE-BSD -``` -Copyright 2019 The Fuchsia Authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -``` - -### License File: LICENSE-MIT -``` -Copyright 2023 The Fuchsia Authors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ``` diff --git a/Cargo.lock b/Cargo.lock index 2b8f07df3..c31bd1839 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -96,6 +102,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "anymap3" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9" + [[package]] name = "arc-swap" version = "1.9.1" @@ -249,7 +261,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -258,6 +279,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -323,6 +353,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -549,6 +585,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam" version = "0.8.4" @@ -605,6 +650,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -646,6 +697,17 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -690,12 +752,30 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "dyn-eq" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" + +[[package]] +name = "dyn-hash" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdab65db9274e0168143841eb8f864a0a21f8b1b8d2ba6812bbe6024346e99e" + [[package]] name = "either" version = "1.15.0" @@ -790,7 +870,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata", "regex-syntax", ] @@ -801,6 +881,16 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -813,6 +903,22 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + [[package]] name = "fluent-uri" version = "0.4.1" @@ -1029,6 +1135,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1047,6 +1165,8 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -1340,6 +1460,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1454,6 +1583,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1487,12 +1622,28 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1509,6 +1660,21 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memo-map" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" + [[package]] name = "micromap" version = "0.3.0" @@ -1521,6 +1687,26 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minijinja" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39" +dependencies = [ + "memo-map", + "serde", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.0" @@ -1598,6 +1784,21 @@ dependencies = [ "libloading", ] +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + [[package]] name = "nemo-relay" version = "0.7.0" @@ -1766,6 +1967,9 @@ dependencies = [ "sha2", "tempfile", "tokio", + "tract-onnx", + "unicode-normalization", + "unicode_categories", ] [[package]] @@ -1865,6 +2069,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "nom-language" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" +dependencies = [ + "nom", +] + [[package]] name = "num" version = "0.4.3" @@ -1948,6 +2161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2134,6 +2348,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pem" version = "3.0.6" @@ -2199,6 +2419,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2233,6 +2462,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2592,6 +2830,42 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand 0.10.1", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -2782,6 +3056,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2860,6 +3148,37 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" +[[package]] +name = "safetensors" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b079b829cb27a1c3c374341345ed2e8b2c0c839034522cee576c140bd7f846" +dependencies = [ + "hashbrown 0.16.1", + "libc", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scan_fmt" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b53b0a5db882a8e2fdaae0a43f7b39e7e9082389e978398bdf223a55b581248" +dependencies = [ + "regex", +] + [[package]] name = "schannel" version = "0.1.29" @@ -3106,6 +3425,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "slab" version = "0.4.12" @@ -3178,6 +3503,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "string-interner" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3df9b59e2eded8d825c7c4363ad339a20fb6bc0b9a4778560f518f59910b15" +dependencies = [ + "hashbrown 0.16.1", + "serde", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3342,6 +3683,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -3735,6 +4087,217 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tract-core" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e176a669d5da02cccc92bbfe5ee4e57686ed8841022608a9eba014d3b7886" +dependencies = [ + "anyhow", + "anymap3", + "bit-set 0.10.0", + "derive-new", + "downcast-rs", + "dyn-clone", + "dyn-eq", + "erased-serde", + "inventory", + "lazy_static", + "log", + "maplit", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pastey", + "rustfft", + "serde", + "smallvec", + "tract-data", + "tract-linalg", +] + +[[package]] +name = "tract-data" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870236dd45aaeb1381023cb709a67ff14ece608ee0b37f99aa166d166db9b0d0" +dependencies = [ + "anyhow", + "downcast-rs", + "dyn-clone", + "dyn-eq", + "dyn-hash", + "half", + "inventory", + "itertools", + "lazy_static", + "libm", + "maplit", + "ndarray", + "nom", + "nom-language", + "num-integer", + "num-traits", + "parking_lot", + "scan_fmt", + "smallvec", + "string-interner", +] + +[[package]] +name = "tract-extra" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effcae1ebfce133e8bf6c79ad31298cbee0a9eceb3e4642fe1d14cb54cca78e6" +dependencies = [ + "tract-nnef", + "tract-pulse", +] + +[[package]] +name = "tract-hir" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28783b2bb583177685f65016a866b01eb3d383bce4fa7ae1b9692b325b64449d" +dependencies = [ + "derive-new", + "log", + "tract-core", +] + +[[package]] +name = "tract-linalg" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e01491f7360806ef061af4c016a2d0a801d586768896394a0b8a7d6872c2b0" +dependencies = [ + "byteorder", + "cc", + "derive-new", + "downcast-rs", + "dyn-clone", + "dyn-eq", + "dyn-hash", + "half", + "lazy_static", + "log", + "minijinja", + "num-traits", + "pastey", + "scan_fmt", + "tract-data", + "walkdir", +] + +[[package]] +name = "tract-nnef" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00417fabf01aeea7bc56107367862e5bb8da18c9ad850c9061d3823700479ecc" +dependencies = [ + "byteorder", + "erased-serde", + "flate2", + "log", + "minijinja", + "nom", + "nom-language", + "safetensors", + "serde", + "serde_json", + "simd-adler32", + "tar", + "tract-core", + "walkdir", +] + +[[package]] +name = "tract-onnx" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3215dd27bddd2a041a20fee750013b400135186d3485501c9274c755b19ceb0" +dependencies = [ + "bytes", + "derive-new", + "dyn-eq", + "log", + "memmap2", + "num-integer", + "prost", + "smallvec", + "tract-extra", + "tract-hir", + "tract-nnef", + "tract-onnx-opl", + "tract-transformers", +] + +[[package]] +name = "tract-onnx-opl" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f549ad3f245c1c00ce710c66cc0e086be0b637f35934d37b655ff175e1ab3e" +dependencies = [ + "dyn-eq", + "getrandom 0.4.2", + "log", + "rand 0.10.1", + "rand_distr", + "rustfft", + "tract-extra", + "tract-nnef", +] + +[[package]] +name = "tract-pulse" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a164e22e96ab9b5c90458fa270700570d87963e3dec9ccaac1ab18d9fa763ce2" +dependencies = [ + "downcast-rs", + "dyn-eq", + "erased-serde", + "lazy_static", + "log", + "serde", + "tract-pulse-opl", + "tract-transformers", +] + +[[package]] +name = "tract-pulse-opl" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65418f9e93e0af0d567f4f2f1bf2309b53930fa2543c635a69fda10c87a04987" +dependencies = [ + "downcast-rs", + "dyn-eq", + "lazy_static", + "tract-nnef", +] + +[[package]] +name = "tract-transformers" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8471ebf7f52d226552283d9130539595540c28ed41b8e032df3830507d3ccdd5" +dependencies = [ + "float-ord", + "rayon", + "tract-nnef", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -3810,6 +4373,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -3828,6 +4400,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3941,6 +4519,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -4112,6 +4700,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -4377,6 +4974,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/Cargo.toml b/Cargo.toml index c77e6e40c..c123a3922 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ nemo-relay-plugin = { version = "0.7.0", path = "crates/plugin" } nemo-relay-worker-proto = { version = "0.7.0", path = "crates/worker-proto" } nemo-relay-worker = { version = "0.7.0", path = "crates/worker" } nemo-relay-adaptive = { version = "0.7.0", path = "crates/adaptive" } -nemo-relay-pii-redaction = { version = "0.7.0", path = "crates/pii-redaction" } +nemo-relay-pii-redaction = { version = "0.7.0", path = "crates/pii-redaction", features = ["rampart"] } nemo-relay-switchyard = { version = "0.7.0", path = "crates/switchyard" } switchyard-translation = "0.1.0" nemo-relay-ffi = { version = "0.7.0", path = "crates/ffi" } diff --git a/crates/cli/src/plugins/editor_model.rs b/crates/cli/src/plugins/editor_model.rs index 5fa842616..d5dc60b5b 100644 --- a/crates/cli/src/plugins/editor_model.rs +++ b/crates/cli/src/plugins/editor_model.rs @@ -14,6 +14,7 @@ use nemo_relay::plugins::nemo_guardrails::component::{ use nemo_relay_adaptive::AdaptiveConfig; use nemo_relay_adaptive::plugin_component::ADAPTIVE_PLUGIN_KIND; use nemo_relay_pii_redaction::component::{PII_REDACTION_PLUGIN_KIND, PiiRedactionConfig}; +use nemo_relay_pii_redaction::rampart::{RAMPART_PII_PLUGIN_KIND, RampartPiiConfig}; #[cfg(feature = "switchyard")] use nemo_relay_switchyard::{SWITCHYARD_PLUGIN_KIND, SwitchyardConfig}; use serde::Serialize; @@ -40,6 +41,7 @@ pub(super) enum EditableComponent { Adaptive(Box>), NemoGuardrails(Box>), PiiRedaction(Box>), + RampartPii(Box>), #[cfg(feature = "switchyard")] Switchyard(Box>), } @@ -51,6 +53,7 @@ impl EditableComponent { Self::Adaptive(_) => "Adaptive", Self::NemoGuardrails(_) => "NeMo Guardrails", Self::PiiRedaction(_) => "PII Redaction", + Self::RampartPii(_) => "Rampart PII", #[cfg(feature = "switchyard")] Self::Switchyard(_) => "Switchyard Decision API", } @@ -62,6 +65,7 @@ impl EditableComponent { Self::Adaptive(_) => AdaptiveConfig::editor_schema().fields, Self::NemoGuardrails(_) => NeMoGuardrailsConfig::editor_schema().fields, Self::PiiRedaction(_) => PiiRedactionConfig::editor_schema().fields, + Self::RampartPii(_) => RampartPiiConfig::editor_schema().fields, #[cfg(feature = "switchyard")] Self::Switchyard(_) => SwitchyardConfig::editor_schema().fields, } @@ -73,6 +77,7 @@ impl EditableComponent { Self::Adaptive(state) => state.enabled, Self::NemoGuardrails(state) => state.enabled, Self::PiiRedaction(state) => state.enabled, + Self::RampartPii(state) => state.enabled, #[cfg(feature = "switchyard")] Self::Switchyard(state) => state.enabled, } @@ -84,6 +89,7 @@ impl EditableComponent { Self::Adaptive(state) => state.toggle_enabled(), Self::NemoGuardrails(state) => state.toggle_enabled(), Self::PiiRedaction(state) => state.toggle_enabled(), + Self::RampartPii(state) => state.toggle_enabled(), #[cfg(feature = "switchyard")] Self::Switchyard(state) => state.toggle_enabled(), } @@ -95,6 +101,7 @@ impl EditableComponent { Self::Adaptive(state) => state.set_enabled(enabled), Self::NemoGuardrails(state) => state.set_enabled(enabled), Self::PiiRedaction(state) => state.set_enabled(enabled), + Self::RampartPii(state) => state.set_enabled(enabled), #[cfg(feature = "switchyard")] Self::Switchyard(state) => state.set_enabled(enabled), } @@ -106,6 +113,7 @@ impl EditableComponent { Self::Adaptive(state) => state.reset_enabled(), Self::NemoGuardrails(state) => state.reset_enabled(), Self::PiiRedaction(state) => state.reset_enabled(), + Self::RampartPii(state) => state.reset_enabled(), #[cfg(feature = "switchyard")] Self::Switchyard(state) => state.reset_enabled(), } @@ -117,6 +125,7 @@ impl EditableComponent { Self::Adaptive(state) => adaptive_summary(state), Self::NemoGuardrails(state) => nemo_guardrails_summary(state), Self::PiiRedaction(state) => pii_redaction_summary(state), + Self::RampartPii(state) => rampart_pii_summary(state), #[cfg(feature = "switchyard")] Self::Switchyard(state) => switchyard_summary(state), } @@ -132,6 +141,9 @@ impl EditableComponent { Self::PiiRedaction(state) => { config_field_configured(&state.config, field).unwrap_or(false) } + Self::RampartPii(state) => { + config_field_configured(&state.config, field).unwrap_or(false) + } #[cfg(feature = "switchyard")] Self::Switchyard(state) => { config_field_configured(&state.config, field).unwrap_or(false) @@ -157,6 +169,10 @@ impl EditableComponent { reset_config_field(&mut state.config, field)?; state.mark_config_touched(); } + Self::RampartPii(state) => { + reset_config_field(&mut state.config, field)?; + state.mark_config_touched(); + } #[cfg(feature = "switchyard")] Self::Switchyard(state) => { reset_config_field(&mut state.config, field)?; @@ -187,6 +203,10 @@ impl EditableComponent { remove_struct_field(&mut state.config, field.name)?; state.mark_config_touched(); } + Self::RampartPii(state) => { + remove_struct_field(&mut state.config, field.name)?; + state.mark_config_touched(); + } #[cfg(feature = "switchyard")] Self::Switchyard(state) => { remove_struct_field(&mut state.config, field.name)?; @@ -202,6 +222,7 @@ impl EditableComponent { Self::Adaptive(state) => store_adaptive_state(config, state), Self::NemoGuardrails(state) => store_nemo_guardrails_state(config, state), Self::PiiRedaction(state) => store_pii_redaction_state(config, state), + Self::RampartPii(state) => store_rampart_pii_state(config, state), #[cfg(feature = "switchyard")] Self::Switchyard(state) => store_switchyard_state(config, state), } @@ -232,6 +253,7 @@ pub(super) fn editable_components( EditableComponent::Adaptive(Box::new(component_adaptive_state(config)?)), EditableComponent::NemoGuardrails(Box::new(component_nemo_guardrails_state(config)?)), EditableComponent::PiiRedaction(Box::new(component_pii_redaction_state(config)?)), + EditableComponent::RampartPii(Box::new(component_rampart_pii_state(config)?)), ]; #[cfg(feature = "switchyard")] let components = { @@ -446,6 +468,12 @@ pub(super) fn component_pii_redaction_state( component_editor_state(config, PII_REDACTION_PLUGIN_KIND, false) } +pub(super) fn component_rampart_pii_state( + config: &PluginConfig, +) -> Result, CliError> { + component_editor_state(config, RAMPART_PII_PLUGIN_KIND, false) +} + #[cfg(feature = "switchyard")] pub(super) fn component_switchyard_state( config: &PluginConfig, @@ -517,6 +545,22 @@ pub(super) fn store_pii_redaction_state( Ok(()) } +pub(super) fn store_rampart_pii_state( + config: &mut PluginConfig, + state: &ComponentEditorState, +) -> Result<(), CliError> { + if state.should_store(state.config_touched || rampart_pii_configured(&state.config)) { + store_component_editor_config( + config, + RAMPART_PII_PLUGIN_KIND, + state.enabled, + rampart_pii_config_map(&state.config)?, + merge_rampart_pii_editor_config, + ); + } + Ok(()) +} + #[cfg(feature = "switchyard")] pub(super) fn store_switchyard_state( config: &mut PluginConfig, @@ -882,6 +926,23 @@ pub(super) fn pii_redaction_config_map( } } +pub(super) fn rampart_pii_config_map( + config: &RampartPiiConfig, +) -> Result, CliError> { + let value = serde_json::to_value(config).map_err(serde_error)?; + match value { + Value::Object(mut map) => { + if is_version_one(map.get("version")) { + map.remove("version"); + } + Ok(map) + } + _ => Err(CliError::Config( + "pii_rampart config must serialize to an object".into(), + )), + } +} + #[cfg(feature = "switchyard")] pub(super) fn switchyard_config_map( config: &SwitchyardConfig, @@ -957,6 +1018,21 @@ pub(super) fn merge_pii_redaction_editor_config( ); } +pub(super) fn merge_rampart_pii_editor_config( + existing: &mut Map, + edited: Map, +) { + if is_version_one(existing.get("version")) { + existing.remove("version"); + } + merge_known_editor_object( + existing, + edited, + &nested_editor_keys(RampartPiiConfig::editor_schema()), + RampartPiiConfig::editor_schema(), + ); +} + #[cfg(feature = "switchyard")] pub(super) fn merge_switchyard_editor_config( existing: &mut Map, @@ -1172,6 +1248,33 @@ pub(super) fn pii_redaction_summary(state: &ComponentEditorState bool { + RampartPiiConfig::editor_schema() + .fields + .iter() + .filter(|field| field.name != POLICY_SECTION) + .any(|field| config_field_configured(config, *field).unwrap_or(false)) +} + +pub(super) fn rampart_pii_summary(state: &ComponentEditorState) -> String { + let configured_fields = RampartPiiConfig::editor_schema() + .fields + .iter() + .filter(|field| field.name != POLICY_SECTION) + .filter(|field| config_field_configured(&state.config, **field).unwrap_or(false)) + .map(|field| field.label) + .collect::>(); + format!( + "component {}, fields {}", + if state.enabled { "enabled" } else { "disabled" }, + if configured_fields.is_empty() { + "none".into() + } else { + configured_fields.join(", ") + } + ) +} + #[cfg(feature = "switchyard")] pub(super) fn switchyard_configured(config: &SwitchyardConfig) -> bool { !config.decision_profile_id.is_empty() || !config.targets.is_empty() diff --git a/crates/cli/src/plugins/mod.rs b/crates/cli/src/plugins/mod.rs index 74116e01e..d9f15cf47 100644 --- a/crates/cli/src/plugins/mod.rs +++ b/crates/cli/src/plugins/mod.rs @@ -354,6 +354,10 @@ fn edit_component_field( edit_config_field(theme, &mut state.config, field)?; state.mark_config_touched(); } + EditableComponent::RampartPii(state) => { + edit_config_field(theme, &mut state.config, field)?; + state.mark_config_touched(); + } #[cfg(feature = "switchyard")] EditableComponent::Switchyard(state) => { edit_config_field(theme, &mut state.config, field)?; diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index a263f0a54..1163e6cc6 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -28,6 +28,7 @@ use nemo_relay::plugin::{ }; use nemo_relay_adaptive::plugin_component::register_adaptive_component; use nemo_relay_pii_redaction::component::register_pii_redaction_component; +use nemo_relay_pii_redaction::rampart::register_rampart_pii_component; #[cfg(feature = "switchyard")] use nemo_relay_switchyard::{ register_switchyard_component, validate_switchyard_atof_configuration, @@ -892,6 +893,7 @@ impl ServerPluginActivation { pub(crate) enum PluginComponentSetupError { Adaptive(String), PiiRedaction(String), + RampartPii(String), #[cfg(feature = "switchyard")] Switchyard(String), #[cfg(feature = "switchyard")] @@ -903,6 +905,7 @@ impl PluginComponentSetupError { match self { Self::Adaptive(_) => "Adaptive plugin", Self::PiiRedaction(_) => "PII redaction plugin", + Self::RampartPii(_) => "Rampart PII plugin", #[cfg(feature = "switchyard")] Self::Switchyard(_) => "Switchyard plugin", #[cfg(feature = "switchyard")] @@ -912,7 +915,7 @@ impl PluginComponentSetupError { pub(crate) fn diagnostic_details(&self) -> String { match self { - Self::Adaptive(error) | Self::PiiRedaction(error) => { + Self::Adaptive(error) | Self::PiiRedaction(error) | Self::RampartPii(error) => { format!("registration failed: {error}") } #[cfg(feature = "switchyard")] @@ -935,6 +938,9 @@ impl std::fmt::Display for PluginComponentSetupError { "PII redaction plugin registration failed: {error}" ) } + Self::RampartPii(error) => { + write!(formatter, "Rampart PII plugin registration failed: {error}") + } #[cfg(feature = "switchyard")] Self::Switchyard(error) => { write!(formatter, "Switchyard plugin registration failed: {error}") @@ -957,6 +963,9 @@ pub(crate) fn register_and_validate_plugin_components( if let Err(error) = register_pii_redaction_component() { errors.push(PluginComponentSetupError::PiiRedaction(error.to_string())); } + if let Err(error) = register_rampart_pii_component() { + errors.push(PluginComponentSetupError::RampartPii(error.to_string())); + } #[cfg(feature = "switchyard")] if let Err(error) = register_switchyard_component() { errors.push(PluginComponentSetupError::Switchyard(error.to_string())); diff --git a/crates/cli/tests/coverage/shared/plugins_tests.rs b/crates/cli/tests/coverage/shared/plugins_tests.rs index b40e38d05..2e6a91b61 100644 --- a/crates/cli/tests/coverage/shared/plugins_tests.rs +++ b/crates/cli/tests/coverage/shared/plugins_tests.rs @@ -19,6 +19,7 @@ use nemo_relay::plugins::nemo_guardrails::component::{ use nemo_relay_adaptive::AdaptiveConfig; use nemo_relay_adaptive::plugin_component::ADAPTIVE_PLUGIN_KIND; use nemo_relay_pii_redaction::component::{PII_REDACTION_PLUGIN_KIND, PiiRedactionConfig}; +use nemo_relay_pii_redaction::rampart::{RAMPART_PII_PLUGIN_KIND, RampartPiiConfig}; use serde_json::Map; use std::path::PathBuf; @@ -399,6 +400,25 @@ fn typed_editor_model_contains_pii_redaction_options() { ); } +#[test] +fn typed_editor_model_contains_rampart_pii_options() { + let schema = RampartPiiConfig::editor_schema(); + assert!(!schema.fields.iter().any(|field| field.name == "version")); + assert_eq!( + schema.field("model_path").unwrap().kind, + EditorFieldKind::String + ); + assert_eq!(schema.field("codec").unwrap().kind, EditorFieldKind::Enum); + assert_eq!( + schema.field("target_path_patterns").unwrap().kind, + EditorFieldKind::List + ); + assert_eq!( + schema.field("inference_batch_size").unwrap().kind, + EditorFieldKind::Integer + ); +} + #[test] fn plugin_menu_uses_setup_theme_markers() { let theme = ColorfulTheme::default(); @@ -1184,6 +1204,52 @@ fn editor_save_preserves_unknown_pii_redaction_fields_and_prunes_version() { assert_eq!(local.get("future_local"), Some(&json!("preserve"))); } +#[test] +fn editor_save_preserves_unknown_rampart_pii_fields_and_prunes_version() { + let mut config = PluginConfig { + components: vec![PluginComponentSpec { + kind: RAMPART_PII_PLUGIN_KIND.to_string(), + enabled: true, + config: json!({ + "version": 1, + "model_path": "/models/rampart", + "target_paths": ["/message"], + "future_top_level": "preserve" + }) + .as_object() + .unwrap() + .clone(), + }], + ..PluginConfig::default() + }; + + let mut rampart = component_rampart_pii_state(&config).unwrap(); + set_struct_field( + &mut rampart.config, + "model_path", + json!("/srv/models/rampart"), + ) + .unwrap(); + rampart.set_enabled(false); + store_rampart_pii_state(&mut config, &rampart).unwrap(); + + let component = config + .components + .iter() + .find(|component| component.kind == RAMPART_PII_PLUGIN_KIND) + .unwrap(); + assert!(!component.enabled); + assert!(!component.config.contains_key("version")); + assert_eq!( + component.config.get("model_path"), + Some(&json!("/srv/models/rampart")) + ); + assert_eq!( + component.config.get("future_top_level"), + Some(&json!("preserve")) + ); +} + #[test] fn adaptive_config_field_reset_handles_optional_and_default_fields() { let mut adaptive = AdaptiveConfig { diff --git a/crates/ffi/src/api/plugin.rs b/crates/ffi/src/api/plugin.rs index 09cd151f8..56f24c8ad 100644 --- a/crates/ffi/src/api/plugin.rs +++ b/crates/ffi/src/api/plugin.rs @@ -20,6 +20,7 @@ use super::{ }; use crate::api::event_registry::Surface; use nemo_relay_pii_redaction::component::register_pii_redaction_component; +use nemo_relay_pii_redaction::rampart::register_rampart_pii_component; struct FfiHostedPluginUserData { ptr: *mut libc::c_void, @@ -133,6 +134,10 @@ fn ensure_pii_redaction_component_registered() -> std::result::Result<(), NemoRe register_pii_redaction_component().map_err(|err| status_from_plugin_error(&err)) } +fn ensure_rampart_pii_component_registered() -> std::result::Result<(), NemoRelayStatus> { + register_rampart_pii_component().map_err(|err| status_from_plugin_error(&err)) +} + fn parse_plugin_config( config_json: *const c_char, ) -> std::result::Result { @@ -237,6 +242,9 @@ pub unsafe extern "C" fn nemo_relay_initialize_with_dynamic_plugins( if let Err(status) = ensure_pii_redaction_component_registered() { return status; } + if let Err(status) = ensure_rampart_pii_component_registered() { + return status; + } let config = match parse_plugin_config(config_json) { Ok(config) => config, Err(status) => return status, @@ -327,6 +335,9 @@ pub unsafe extern "C" fn nemo_relay_validate_plugin_config( if let Err(status) = ensure_pii_redaction_component_registered() { return status; } + if let Err(status) = ensure_rampart_pii_component_registered() { + return status; + } let config_value = match c_str_to_json(config_json) { Some(value) => value, None => return NemoRelayStatus::InvalidJson, @@ -369,6 +380,9 @@ pub unsafe extern "C" fn nemo_relay_initialize_plugins( if let Err(status) = ensure_pii_redaction_component_registered() { return status; } + if let Err(status) = ensure_rampart_pii_component_registered() { + return status; + } let config_value = match c_str_to_json(config_json) { Some(value) => value, None => return NemoRelayStatus::InvalidJson, @@ -448,6 +462,9 @@ pub unsafe extern "C" fn nemo_relay_list_plugin_kinds_json( if let Err(status) = ensure_pii_redaction_component_registered() { return status; } + if let Err(status) = ensure_rampart_pii_component_registered() { + return status; + } let kinds_json = match serde_json::to_value(list_plugin_kinds()) { Ok(value) => value, Err(err) => { diff --git a/crates/node/package.json b/crates/node/package.json index 6f2f371a4..d01e23a5d 100644 --- a/crates/node/package.json +++ b/crates/node/package.json @@ -49,6 +49,10 @@ "types": "./pii_redaction.d.ts", "default": "./pii_redaction.js" }, + "./pii_rampart": { + "types": "./pii_rampart.d.ts", + "default": "./pii_rampart.js" + }, "./model_pricing": { "types": "./model_pricing.d.ts", "default": "./model_pricing.js" diff --git a/crates/node/pii_rampart.d.ts b/crates/node/pii_rampart.d.ts new file mode 100644 index 000000000..ed33e9b71 --- /dev/null +++ b/crates/node/pii_rampart.d.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export interface ConfigPolicy { + unknown_component?: 'ignore' | 'warn' | 'error' | string; + unknown_field?: 'ignore' | 'warn' | 'error' | string; + unsupported_value?: 'ignore' | 'warn' | 'error' | string; +} + +export interface Config { + version?: number; + model_path: string; + input?: boolean; + output?: boolean; + mark?: boolean; + tool_input?: boolean; + tool_output?: boolean; + priority?: number; + codec?: 'openai_chat' | 'openai_responses' | 'anthropic_messages' | string; + target_paths?: string[]; + target_path_patterns?: string[]; + min_score?: number; + excluded_labels?: string[]; + replacement?: string; + max_windows_per_payload?: number; + inference_batch_size?: number; + policy?: ConfigPolicy; +} + +export declare const RAMPART_PII_PLUGIN_KIND: 'pii_rampart'; +export declare const RAMPART_MODEL_ID: 'nationaldesignstudio/rampart'; +export declare const RAMPART_MODEL_REVISION: 'b1993e4e68b082835b80ffc65acc03325ea2e501'; +export declare function defaultConfig(modelPath: string, config?: Partial): Config; +export declare function ComponentSpec( + config: Config, + options?: { enabled?: boolean }, +): import('./plugin.js').ComponentSpec; +export declare function validateConfig(config: Config): import('./plugin.js').ConfigReport; diff --git a/crates/node/pii_rampart.js b/crates/node/pii_rampart.js new file mode 100644 index 000000000..1cc6d7ab7 --- /dev/null +++ b/crates/node/pii_rampart.js @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; + +const plugin = require('./plugin.js'); + +const RAMPART_PII_PLUGIN_KIND = 'pii_rampart'; +const RAMPART_MODEL_ID = 'nationaldesignstudio/rampart'; +const RAMPART_MODEL_REVISION = 'b1993e4e68b082835b80ffc65acc03325ea2e501'; + +/** + * Create Rampart PII settings with runtime defaults applied. + * + * @param {string} modelPath - Absolute path to the pinned Rampart snapshot. + * @param {object} [config={}] - Partial settings to override. + * @returns {object} A normalized Rampart PII config object. + */ +function defaultConfig(modelPath, config = {}) { + return { + version: 1, + model_path: modelPath, + input: true, + output: true, + mark: true, + tool_input: true, + tool_output: true, + priority: 100, + target_paths: [], + target_path_patterns: [], + min_score: 0.4, + excluded_labels: [], + replacement: '[REDACTED]', + max_windows_per_payload: 128, + inference_batch_size: 16, + ...config, + }; +} + +/** + * Wrap Rampart PII config as a top-level plugin component. + * + * @param {object} config - Rampart PII component configuration. + * @param {{ enabled?: boolean }} [options={}] - Optional component flags. + * @returns {object} A shared plugin component spec. + */ +function ComponentSpec(config, { enabled = true } = {}) { + return plugin.ComponentSpec(RAMPART_PII_PLUGIN_KIND, config, { enabled }); +} + +/** + * Validate Rampart PII configuration without loading model files. + * + * @param {object} config - Rampart PII component configuration. + * @returns {object} A structured validation report with diagnostics. + */ +function validateConfig(config) { + return plugin.validate({ + version: 1, + components: [ComponentSpec(config)], + }); +} + +module.exports = { + RAMPART_PII_PLUGIN_KIND, + RAMPART_MODEL_ID, + RAMPART_MODEL_REVISION, + defaultConfig, + ComponentSpec, + validateConfig, +}; diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 1f8384fae..c4cb24238 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -68,6 +68,7 @@ use nemo_relay_adaptive::context_helpers::set_latency_sensitivity as adaptive_se use nemo_relay_adaptive::plugin_component::register_adaptive_component; use nemo_relay_adaptive::{AdaptiveConfig, AdaptiveRuntime as CoreAdaptiveRuntime}; use nemo_relay_pii_redaction::component::register_pii_redaction_component; +use nemo_relay_pii_redaction::rampart::register_rampart_pii_component; use crate::callable; use crate::convert::{ @@ -89,6 +90,8 @@ fn init() { .expect("node adaptive plugin component registration should succeed"); register_pii_redaction_component() .expect("node pii redaction plugin component registration should succeed"); + register_rampart_pii_component() + .expect("node Rampart PII plugin component registration should succeed"); } #[cfg(not(test))] diff --git a/crates/node/tests/pii_rampart_tests.mjs b/crates/node/tests/pii_rampart_tests.mjs new file mode 100644 index 000000000..1605580c2 --- /dev/null +++ b/crates/node/tests/pii_rampart_tests.mjs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const rampart = require('../pii_rampart.js'); + +describe('pii_rampart plugin helpers', () => { + it('builds the independent component shape', () => { + const config = rampart.defaultConfig('/models/rampart', { + codec: 'openai_chat', + target_path_patterns: ['/messages/*/content'], + }); + assert.equal(config.model_path, '/models/rampart'); + assert.equal(config.inference_batch_size, 16); + assert.equal(rampart.RAMPART_MODEL_ID, 'nationaldesignstudio/rampart'); + assert.equal(rampart.RAMPART_MODEL_REVISION, 'b1993e4e68b082835b80ffc65acc03325ea2e501'); + const component = rampart.ComponentSpec(config); + assert.equal(component.kind, rampart.RAMPART_PII_PLUGIN_KIND); + assert.equal(component.enabled, true); + }); + + it('is registered and validates malformed paths', () => { + const plugin = require('../plugin.js'); + assert.equal(plugin.listKinds().includes(rampart.RAMPART_PII_PLUGIN_KIND), true); + const report = rampart.validateConfig( + rampart.defaultConfig('relative/model', { + target_path_patterns: ['/messages/pre*fix/content'], + }), + ); + assert.deepEqual( + new Set(report.diagnostics.map((diagnostic) => diagnostic.field)), + new Set(['model_path', 'target_path_patterns']), + ); + }); +}); diff --git a/crates/pii-redaction/Cargo.toml b/crates/pii-redaction/Cargo.toml index 006a45b59..bd2244a83 100644 --- a/crates/pii-redaction/Cargo.toml +++ b/crates/pii-redaction/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [features] default = [] +rampart = ["dep:tokio", "dep:tract-onnx", "dep:unicode_categories", "dep:unicode-normalization"] schema = ["dep:schemars", "nemo-relay/schema"] [dependencies] @@ -25,6 +26,10 @@ serde_json = "1" regex = "1" sha2 = "0.11" schemars = { version = "0.8", optional = true } +tokio = { version = "1", features = ["rt"], optional = true } +tract-onnx = { version = "0.23.4", optional = true } +unicode_categories = { version = "0.1.1", optional = true } +unicode-normalization = { version = "0.1.25", optional = true } [dev-dependencies] nemo-relay = { workspace = true, features = ["openinference", "otel"] } diff --git a/crates/pii-redaction/src/builtin.rs b/crates/pii-redaction/src/builtin.rs index 6e6d1dc5c..dc9e6b5a8 100644 --- a/crates/pii-redaction/src/builtin.rs +++ b/crates/pii-redaction/src/builtin.rs @@ -708,7 +708,7 @@ fn render_json_pointer_path(path_segments: &[String]) -> String { rendered } -fn escape_json_pointer_segment(segment: &str) -> String { +pub(crate) fn escape_json_pointer_segment(segment: &str) -> String { segment.replace('~', "~0").replace('/', "~1") } diff --git a/crates/pii-redaction/src/lib.rs b/crates/pii-redaction/src/lib.rs index 0abb15432..119f88b7f 100644 --- a/crates/pii-redaction/src/lib.rs +++ b/crates/pii-redaction/src/lib.rs @@ -13,6 +13,8 @@ pub mod component; pub(crate) mod detectors; pub(crate) mod local; pub(crate) mod overlay; +#[cfg(feature = "rampart")] +pub mod rampart; pub(crate) mod trajectory; #[cfg(test)] diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs new file mode 100644 index 000000000..e76e91141 --- /dev/null +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -0,0 +1,703 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! In-process Rampart PII redaction plugin. + +use std::collections::HashSet; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; + +use nemo_relay::codec::resolve::supported_codec_names; +use nemo_relay::plugin::{ + ConfigDiagnostic, ConfigPolicy, DiagnosticLevel, Plugin, PluginComponentSpec, PluginError, + PluginRegistrationContext, Result as PluginResult, UnsupportedBehavior, + apply_global_config_policy, deregister_plugin, register_plugin, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value as Json}; + +mod model; +mod sanitizer; +mod tokenizer; + +use model::RampartDetector; +use sanitizer::{ + RampartSanitizer, event_sanitize_callback, llm_sanitize_request_callback, + llm_sanitize_response_callback, tool_sanitize_callback, +}; + +/// Plugin kind for in-process Rampart PII redaction. +pub const RAMPART_PII_PLUGIN_KIND: &str = "pii_rampart"; +/// Pinned Hugging Face model repository used by this plugin. +pub const RAMPART_MODEL_ID: &str = "nationaldesignstudio/rampart"; +/// Pinned model revision whose files are accepted by this plugin. +pub const RAMPART_MODEL_REVISION: &str = "b1993e4e68b082835b80ffc65acc03325ea2e501"; + +const MAX_MODEL_PATH_BYTES: usize = 4096; +const MAX_TARGET_PATHS: usize = 256; +const MAX_TARGET_PATH_BYTES: usize = 1024; +const MAX_EXCLUDED_LABELS: usize = 128; +const MAX_LABEL_BYTES: usize = 128; +const MAX_REPLACEMENT_BYTES: usize = 1024; + +/// One configured Rampart PII component. +#[derive(Debug, Clone)] +pub struct ComponentSpec { + /// Whether the component should be activated. + pub enabled: bool, + /// Component-local Rampart configuration. + pub config: RampartPiiConfig, +} + +impl ComponentSpec { + /// Creates an enabled Rampart PII component spec. + pub fn new(config: RampartPiiConfig) -> Self { + Self { + enabled: true, + config, + } + } +} + +impl From for PluginComponentSpec { + fn from(value: ComponentSpec) -> Self { + let Json::Object(config) = serde_json::to_value(value.config) + .expect("Rampart PII config should serialize to an object") + else { + unreachable!("Rampart PII config must serialize to an object"); + }; + PluginComponentSpec { + kind: RAMPART_PII_PLUGIN_KIND.to_string(), + enabled: value.enabled, + config, + } + } +} + +/// Configuration for the in-process Rampart PII component. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub struct RampartPiiConfig { + /// Configuration schema version. + #[serde(default = "default_config_version")] + pub version: u32, + /// Local directory containing the pinned Rampart snapshot. + pub model_path: String, + /// Whether to sanitize managed LLM request payloads. + #[serde(default = "default_true")] + pub input: bool, + /// Whether to sanitize managed LLM response payloads. + #[serde(default = "default_true")] + pub output: bool, + /// Whether to sanitize mark event observability fields. + #[serde(default = "default_true")] + pub mark: bool, + /// Whether to sanitize managed tool request payloads. + #[serde(default = "default_true")] + pub tool_input: bool, + /// Whether to sanitize managed tool response payloads. + #[serde(default = "default_true")] + pub tool_output: bool, + /// Guardrail priority. Lower values run earlier. + #[serde(default = "default_priority")] + pub priority: i32, + /// Compatibility codec for calls without an active per-call codec. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "schema", schemars(schema_with = "codec_schema"))] + pub codec: Option, + /// Exact JSON-pointer paths selected for model inspection. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub target_paths: Vec, + /// JSON-pointer patterns selected for model inspection. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub target_path_patterns: Vec, + /// Minimum model confidence accepted for redaction. + #[serde(default = "default_min_score")] + pub min_score: f64, + /// Exact, case-sensitive model labels that remain visible. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub excluded_labels: Vec, + /// Replacement applied to accepted model spans and failed batches. + #[serde(default = "default_replacement")] + pub replacement: String, + /// Maximum token windows accepted from one observability payload. + #[serde(default = "default_max_windows_per_payload")] + pub max_windows_per_payload: usize, + /// Maximum short token windows grouped in one model invocation. + #[serde(default = "default_inference_batch_size")] + pub inference_batch_size: usize, + /// Component-local unsupported-config policy. + #[serde(default)] + pub policy: ConfigPolicy, +} + +impl Default for RampartPiiConfig { + fn default() -> Self { + Self { + version: default_config_version(), + model_path: String::new(), + input: true, + output: true, + mark: true, + tool_input: true, + tool_output: true, + priority: default_priority(), + codec: None, + target_paths: Vec::new(), + target_path_patterns: Vec::new(), + min_score: default_min_score(), + excluded_labels: Vec::new(), + replacement: default_replacement(), + max_windows_per_payload: default_max_windows_per_payload(), + inference_batch_size: default_inference_batch_size(), + policy: ConfigPolicy::default(), + } + } +} + +nemo_relay::editor_config! { + impl RampartPiiConfig { + model_path => { label: "model_path", kind: String }, + input => { label: "input", kind: Boolean }, + output => { label: "output", kind: Boolean }, + mark => { label: "mark", kind: Boolean }, + tool_input => { label: "tool_input", kind: Boolean }, + tool_output => { label: "tool_output", kind: Boolean }, + priority => { label: "priority", kind: Integer }, + codec => { + label: "codec", + kind: Enum, + values: ["openai_chat", "openai_responses", "anthropic_messages"], + optional: true, + }, + target_paths => { + label: "target_paths", + kind: List, + list: &nemo_relay::config_editor::STRING_LIST_ITEM, + }, + target_path_patterns => { + label: "target_path_patterns", + kind: List, + list: &nemo_relay::config_editor::STRING_LIST_ITEM, + }, + min_score => { label: "min_score", kind: Float }, + excluded_labels => { + label: "excluded_labels", + kind: List, + list: &nemo_relay::config_editor::STRING_LIST_ITEM, + }, + replacement => { label: "replacement", kind: String }, + max_windows_per_payload => { label: "max_windows_per_payload", kind: Integer }, + inference_batch_size => { label: "inference_batch_size", kind: Integer }, + policy => { + label: "policy", + kind: Section, + nested: ConfigPolicy, + default: ConfigPolicy, + }, + } +} + +struct RampartPiiPlugin; + +impl Plugin for RampartPiiPlugin { + fn plugin_kind(&self) -> &str { + RAMPART_PII_PLUGIN_KIND + } + + fn allows_multiple_components(&self) -> bool { + false + } + + fn validate(&self, plugin_config: &Map) -> Vec { + validate_rampart_pii_config(plugin_config, None) + } + + fn validate_with_policy( + &self, + plugin_config: &Map, + policy: &ConfigPolicy, + ) -> Vec { + validate_rampart_pii_config(plugin_config, Some(policy)) + } + + fn register<'a>( + &'a self, + plugin_config: &Map, + ctx: &'a mut PluginRegistrationContext, + ) -> Pin> + Send + 'a>> { + let parsed = parse_config(plugin_config); + Box::pin(async move { + let config = parsed?; + let model_path = PathBuf::from(&config.model_path); + let max_windows = config.max_windows_per_payload; + let batch_size = config.inference_batch_size; + let detector = tokio::task::spawn_blocking(move || { + RampartDetector::load(model_path, max_windows, batch_size) + }) + .await + .map_err(|error| { + PluginError::Internal(format!("Rampart model initialization task failed: {error}")) + })??; + let sanitizer = RampartSanitizer::new(config.clone(), Arc::new(detector))?; + register_sanitizers(&config, sanitizer, ctx)?; + log::info!( + target: "nemo_relay.plugin", + event = "plugin_resource_validation_completed", + plugin_kind = RAMPART_PII_PLUGIN_KIND, + model_id = RAMPART_MODEL_ID, + model_revision = RAMPART_MODEL_REVISION, + resource_count = 1; + "Rampart PII model loaded in the Relay process" + ); + Ok(()) + }) + } +} + +/// Registers the `pii_rampart` component kind. +pub fn register_rampart_pii_component() -> PluginResult<()> { + match register_plugin(Arc::new(RampartPiiPlugin)) { + Ok(()) => Ok(()), + Err(PluginError::RegistrationFailed(message)) if message.contains("already registered") => { + Ok(()) + } + Err(error) => Err(error), + } +} + +/// Deregisters the `pii_rampart` component kind. +pub fn deregister_rampart_pii_component() -> bool { + deregister_plugin(RAMPART_PII_PLUGIN_KIND) +} + +/// Returns the JSON Schema for Rampart PII configuration. +#[cfg(feature = "schema")] +pub fn rampart_pii_config_schema() -> Json { + serde_json::to_value(schemars::schema_for!(RampartPiiConfig)) + .expect("Rampart PII config schema should serialize") +} + +fn register_sanitizers( + config: &RampartPiiConfig, + sanitizer: RampartSanitizer, + ctx: &mut PluginRegistrationContext, +) -> PluginResult<()> { + if config.mark { + ctx.register_mark_sanitize_guardrail( + "mark", + config.priority, + event_sanitize_callback(sanitizer.clone(), None), + )?; + } + if config.tool_input { + ctx.register_tool_sanitize_request_guardrail( + "tool_input", + config.priority, + tool_sanitize_callback(sanitizer.clone()), + )?; + } + if config.tool_output { + ctx.register_tool_sanitize_response_guardrail( + "tool_output", + config.priority, + tool_sanitize_callback(sanitizer.clone()), + )?; + } + if config.input { + ctx.register_llm_sanitize_request_guardrail( + "input", + config.priority, + llm_sanitize_request_callback(sanitizer.clone()), + )?; + } + if config.input || config.tool_input { + ctx.register_scope_sanitize_start_guardrail( + "scope_start", + config.priority, + event_sanitize_callback(sanitizer.clone(), Some((config.input, config.tool_input))), + )?; + } + if config.output { + ctx.register_llm_sanitize_response_guardrail( + "output", + config.priority, + llm_sanitize_response_callback(sanitizer.clone()), + )?; + } + if config.output || config.tool_output { + ctx.register_scope_sanitize_end_guardrail( + "scope_end", + config.priority, + event_sanitize_callback(sanitizer, Some((config.output, config.tool_output))), + )?; + } + Ok(()) +} + +fn parse_config(plugin_config: &Map) -> PluginResult { + serde_json::from_value(Json::Object(plugin_config.clone())).map_err(|error| { + PluginError::InvalidConfig(format!("invalid Rampart PII plugin config: {error}")) + }) +} + +fn validate_rampart_pii_config( + plugin_config: &Map, + global_policy: Option<&ConfigPolicy>, +) -> Vec { + let mut config = match parse_config(plugin_config) { + Ok(config) => config, + Err(error) => { + return vec![ConfigDiagnostic { + level: DiagnosticLevel::Error, + code: "pii_rampart.invalid_plugin_config".into(), + component: Some(RAMPART_PII_PLUGIN_KIND.into()), + field: None, + message: error.to_string(), + }]; + } + }; + if let Some(global_policy) = global_policy { + config.policy = apply_global_config_policy(config.policy, global_policy); + } + + let mut diagnostics = Vec::new(); + let supported = [ + "version", + "model_path", + "input", + "output", + "mark", + "tool_input", + "tool_output", + "priority", + "codec", + "target_paths", + "target_path_patterns", + "min_score", + "excluded_labels", + "replacement", + "max_windows_per_payload", + "inference_batch_size", + "policy", + ]; + for field in plugin_config.keys() { + if !supported.contains(&field.as_str()) { + push_diagnostic( + &mut diagnostics, + config.policy.unknown_field, + "pii_rampart.unknown_field", + Some(field.clone()), + format!("unknown field '{field}'"), + ); + } + } + if let Some(Json::Object(policy)) = plugin_config.get("policy") { + for field in policy.keys() { + if !["unknown_component", "unknown_field", "unsupported_value"] + .contains(&field.as_str()) + { + push_diagnostic( + &mut diagnostics, + config.policy.unknown_field, + "pii_rampart.unknown_field", + Some(format!("policy.{field}")), + format!("unknown field 'policy.{field}'"), + ); + } + } + } + + if config.version != default_config_version() { + push_unsupported( + &mut diagnostics, + &config, + "version", + format!( + "Rampart PII config version {} is unsupported", + config.version + ), + ); + } + if config.model_path.trim().is_empty() || config.model_path.len() > MAX_MODEL_PATH_BYTES { + push_unsupported( + &mut diagnostics, + &config, + "model_path", + format!("model_path must be non-empty and at most {MAX_MODEL_PATH_BYTES} UTF-8 bytes"), + ); + } else if !PathBuf::from(&config.model_path).is_absolute() { + push_unsupported( + &mut diagnostics, + &config, + "model_path", + "model_path must be absolute".into(), + ); + } + if !(config.input || config.output || config.mark || config.tool_input || config.tool_output) { + push_unsupported( + &mut diagnostics, + &config, + "input", + "at least one sanitization surface must be enabled".into(), + ); + } + if let Some(codec) = config.codec.as_deref() + && !supported_codec_names().contains(&codec) + { + push_unsupported( + &mut diagnostics, + &config, + "codec", + "codec must be 'openai_chat', 'openai_responses', or 'anthropic_messages'".into(), + ); + } + if config.target_paths.is_empty() && config.target_path_patterns.is_empty() { + push_unsupported( + &mut diagnostics, + &config, + "target_paths", + "target_paths or target_path_patterns must select explicit content fields".into(), + ); + } + if config.target_paths.len() + config.target_path_patterns.len() > MAX_TARGET_PATHS { + push_unsupported( + &mut diagnostics, + &config, + "target_paths", + format!( + "target_paths and target_path_patterns must contain at most {MAX_TARGET_PATHS} entries" + ), + ); + } + if config + .target_paths + .iter() + .any(|path| path.len() > MAX_TARGET_PATH_BYTES || !is_valid_json_pointer(path)) + { + push_unsupported( + &mut diagnostics, + &config, + "target_paths", + "target_paths entries must be bounded valid JSON pointers".into(), + ); + } + if config + .target_path_patterns + .iter() + .any(|path| path.len() > MAX_TARGET_PATH_BYTES || !is_valid_json_pointer_pattern(path)) + { + push_unsupported( + &mut diagnostics, + &config, + "target_path_patterns", + "target_path_patterns entries must be bounded JSON pointers with only complete '*' segments".into(), + ); + } + if !config.min_score.is_finite() || !(0.0..=1.0).contains(&config.min_score) { + push_unsupported( + &mut diagnostics, + &config, + "min_score", + "min_score must be a finite number between 0 and 1".into(), + ); + } + if config.excluded_labels.len() > MAX_EXCLUDED_LABELS + || config + .excluded_labels + .iter() + .any(|label| label.trim().is_empty() || label.len() > MAX_LABEL_BYTES) + || config.excluded_labels.iter().collect::>().len() + != config.excluded_labels.len() + { + push_unsupported( + &mut diagnostics, + &config, + "excluded_labels", + format!( + "excluded_labels must contain at most {MAX_EXCLUDED_LABELS} unique, bounded labels" + ), + ); + } + if config.replacement.len() > MAX_REPLACEMENT_BYTES { + push_unsupported( + &mut diagnostics, + &config, + "replacement", + format!("replacement must not exceed {MAX_REPLACEMENT_BYTES} UTF-8 bytes"), + ); + } + if !(1..=512).contains(&config.max_windows_per_payload) { + push_unsupported( + &mut diagnostics, + &config, + "max_windows_per_payload", + "max_windows_per_payload must be between 1 and 512".into(), + ); + } + if !(1..=64).contains(&config.inference_batch_size) { + push_unsupported( + &mut diagnostics, + &config, + "inference_batch_size", + "inference_batch_size must be between 1 and 64".into(), + ); + } + diagnostics +} + +fn push_unsupported( + diagnostics: &mut Vec, + config: &RampartPiiConfig, + field: &str, + message: String, +) { + push_diagnostic( + diagnostics, + config.policy.unsupported_value, + "pii_rampart.unsupported_value", + Some(field.into()), + message, + ); +} + +fn push_diagnostic( + diagnostics: &mut Vec, + behavior: UnsupportedBehavior, + code: &str, + field: Option, + message: String, +) { + let level = match behavior { + UnsupportedBehavior::Ignore => return, + UnsupportedBehavior::Warn => DiagnosticLevel::Warning, + UnsupportedBehavior::Error => DiagnosticLevel::Error, + }; + diagnostics.push(ConfigDiagnostic { + level, + code: code.into(), + component: Some(RAMPART_PII_PLUGIN_KIND.into()), + field, + message, + }); +} + +fn is_valid_json_pointer(path: &str) -> bool { + if path.is_empty() { + return true; + } + if !path.starts_with('/') { + return false; + } + let mut bytes = path.as_bytes().iter().copied(); + while let Some(byte) = bytes.next() { + if byte == b'~' && !matches!(bytes.next(), Some(b'0' | b'1')) { + return false; + } + } + true +} + +fn is_valid_json_pointer_pattern(path: &str) -> bool { + is_valid_json_pointer(path) + && path + .strip_prefix('/') + .unwrap_or_default() + .split('/') + .all(|segment| !segment.contains('*') || segment == "*") +} + +fn default_config_version() -> u32 { + 1 +} + +fn default_true() -> bool { + true +} + +fn default_priority() -> i32 { + 100 +} + +fn default_min_score() -> f64 { + 0.4 +} + +fn default_replacement() -> String { + "[REDACTED]".into() +} + +fn default_max_windows_per_payload() -> usize { + 128 +} + +fn default_inference_batch_size() -> usize { + 16 +} + +#[cfg(feature = "schema")] +fn codec_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + let mut schema: schemars::schema::SchemaObject = + ::json_schema(generator).into(); + schema.enum_values = Some( + ["openai_chat", "openai_responses", "anthropic_messages"] + .into_iter() + .map(|value| Json::String(value.into())) + .collect(), + ); + schema.into() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn valid_config() -> Map { + let Json::Object(config) = serde_json::to_value(RampartPiiConfig { + model_path: "/tmp/rampart".into(), + target_path_patterns: vec!["/messages/*/content".into()], + ..RampartPiiConfig::default() + }) + .unwrap() else { + unreachable!() + }; + config + } + + #[test] + fn validates_explicit_model_and_content_paths() { + assert!(validate_rampart_pii_config(&valid_config(), None).is_empty()); + + let mut config = valid_config(); + config.insert("model_path".into(), Json::String("relative/model".into())); + config.insert( + "target_path_patterns".into(), + serde_json::json!(["/messages/pre*fix/content"]), + ); + let diagnostics = validate_rampart_pii_config(&config, None); + assert!( + diagnostics + .iter() + .any(|item| item.field.as_deref() == Some("model_path")) + ); + assert!( + diagnostics + .iter() + .any(|item| item.field.as_deref() == Some("target_path_patterns")) + ); + } + + #[test] + fn component_spec_uses_independent_plugin_kind() { + let spec: PluginComponentSpec = ComponentSpec::new(RampartPiiConfig { + model_path: "/tmp/rampart".into(), + target_paths: vec!["/message".into()], + ..RampartPiiConfig::default() + }) + .into(); + assert_eq!(spec.kind, RAMPART_PII_PLUGIN_KIND); + assert_ne!(spec.kind, crate::component::PII_REDACTION_PLUGIN_KIND); + } +} diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs new file mode 100644 index 000000000..46e9f0407 --- /dev/null +++ b/crates/pii-redaction/src/rampart/model.rs @@ -0,0 +1,704 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::fmt::Write as _; +use std::fs::File; +use std::io::{BufReader, Read, Seek, SeekFrom}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use nemo_relay::plugin::{PluginError, Result as PluginResult}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use tract_onnx::prelude::*; + +use super::tokenizer::RampartTokenizer; + +const MODEL_MAX_TOKENS: usize = 512; +const SPECIAL_TOKEN_COUNT: usize = 2; +const CONTENT_TOKEN_BUDGET: usize = MODEL_MAX_TOKENS - SPECIAL_TOKEN_COUNT; +const WINDOW_OVERLAP_TOKENS: usize = 64; +const MAX_PADDED_TOKENS_PER_BATCH: usize = MODEL_MAX_TOKENS; + +const MODEL_FILES: &[(&str, &str)] = &[ + ( + "config.json", + "003b84bbcd489f5e782fe5cad8f3249c3653ec880089abb1ccc398a0d895e3e6", + ), + ( + "onnx/model_q4.onnx", + "9f27d24949b0581701071ea5ef522d77ccd3f50c525cc91eac4d265b0fc2afe5", + ), + ( + "special_tokens_map.json", + "5d5b662e421ea9fac075174bb0688ee0d9431699900b90662acd44b2a350503a", + ), + ( + "tokenizer.json", + "98ade711428b42a1b5343c403a73344535e92de8e19359cdb567ef34da210259", + ), + ( + "tokenizer_config.json", + "0088a6f8bcdd4014184fb068b83ebb12896a9db2bb269a71f73de83fef24bceb", + ), + ( + "vocab.txt", + "0fbe6b50061feabb9be68af471e9aa6df07a4bc428bdca4b0eff1fcd3612dee5", + ), +]; + +#[derive(Clone, Debug)] +pub(super) struct Detection { + pub(super) text_index: usize, + pub(super) start_utf8: usize, + pub(super) end_utf8: usize, + pub(super) label: String, + pub(super) score: f64, +} + +pub(super) struct RampartDetector { + tokenizer: RampartTokenizer, + plan: Arc, + labels: Arc<[String]>, + cls_id: i64, + sep_id: i64, + pad_id: i64, + max_windows_per_payload: usize, + inference_batch_size: usize, + // Tokenization and inference share one admission point to bound aggregate + // request-local tensor memory under concurrent Relay traffic. + inference_lock: Mutex<()>, +} + +#[derive(Deserialize)] +struct ModelConfig { + id2label: HashMap, +} + +struct VerifiedModelFiles { + config: File, + model: File, + vocab: File, +} + +struct Window { + text_index: usize, + input_ids: Vec, + token_type_ids: Vec, + offsets: Vec>, +} + +#[derive(Clone)] +struct Span { + start: usize, + end: usize, + label: String, + score: f64, +} + +struct SpanAccumulator { + start: usize, + end: usize, + label: String, + score_total: f64, + token_count: usize, +} + +impl SpanAccumulator { + fn finish(self) -> Span { + Span { + start: self.start, + end: self.end, + label: self.label, + score: self.score_total / self.token_count as f64, + } + } +} + +impl RampartDetector { + pub(super) fn load( + model_root: PathBuf, + max_windows_per_payload: usize, + inference_batch_size: usize, + ) -> PluginResult { + let model_root = model_root.canonicalize().map_err(|error| { + invalid_model(format!( + "Rampart model directory '{}' is unavailable: {error}", + model_root.display() + )) + })?; + if !model_root.is_dir() { + return Err(invalid_model(format!( + "Rampart model path '{}' is not a directory", + model_root.display() + ))); + } + let files = verify_model_files(&model_root)?; + + let config: ModelConfig = serde_json::from_reader(BufReader::new(files.config)) + .map_err(|error| invalid_model(format!("invalid Rampart config.json: {error}")))?; + let labels = parse_labels(config.id2label)?; + + let tokenizer = RampartTokenizer::from_vocab_reader(BufReader::new(files.vocab))?; + let cls_id = required_token_id(&tokenizer, "[CLS]")?; + let sep_id = required_token_id(&tokenizer, "[SEP]")?; + let pad_id = required_token_id(&tokenizer, "[PAD]")?; + + let framework = tract_onnx::onnx() + .with_ignore_value_info(true) + .with_ignore_output_shapes(true); + let mut model_reader = BufReader::new(files.model); + let model = framework + .model_for_read(&mut model_reader) + .map_err(|error| { + invalid_model(format!("failed to load Rampart ONNX model: {error}")) + })?; + validate_model_inputs(&model)?; + let outputs = model + .output_outlets() + .map_err(|error| invalid_model(format!("invalid Rampart model outputs: {error}")))?; + if outputs.len() != 1 { + return Err(invalid_model( + "Rampart ONNX model must expose exactly one logits output", + )); + } + let plan = model + .into_optimized() + .and_then(|model| model.into_runnable()) + .map_err(|error| { + invalid_model(format!("failed to optimize Rampart ONNX model: {error}")) + })?; + + let detector = Self { + tokenizer, + plan, + labels: labels.into(), + cls_id, + sep_id, + pad_id, + max_windows_per_payload, + inference_batch_size, + inference_lock: Mutex::new(()), + }; + detector.detect(&["warmup"])?; + Ok(detector) + } + + pub(super) fn detect(&self, texts: &[&str]) -> PluginResult> { + let _guard = self.inference_lock.lock().map_err(|error| { + PluginError::Internal(format!("Rampart inference lock poisoned: {error}")) + })?; + let windows = self.build_windows(texts)?; + if windows.is_empty() { + return Ok(Vec::new()); + } + + let mut spans_by_text = vec![Vec::new(); texts.len()]; + for batch in inference_batches(&windows, self.inference_batch_size) { + let logits = self.infer_batch(&windows, &batch)?; + for (batch_index, window_index) in batch.iter().copied().enumerate() { + let window = &windows[window_index]; + spans_by_text[window.text_index].extend(self.decode_window( + window, + &logits, + batch_index, + )?); + } + } + + let mut detections = Vec::new(); + for (text_index, spans) in spans_by_text.into_iter().enumerate() { + for span in merge_overlapping_spans(spans) { + let text = texts[text_index]; + if span.start >= span.end + || span.end > text.len() + || !text.is_char_boundary(span.start) + || !text.is_char_boundary(span.end) + { + return Err(inference_error( + "Rampart tokenizer returned an invalid UTF-8 span", + )); + } + detections.push(Detection { + text_index, + start_utf8: span.start, + end_utf8: span.end, + label: span.label, + score: span.score, + }); + } + } + Ok(detections) + } + + fn build_windows(&self, texts: &[&str]) -> PluginResult> { + let step = CONTENT_TOKEN_BUDGET - WINDOW_OVERLAP_TOKENS; + let mut windows = Vec::new(); + for (text_index, text) in texts.iter().copied().enumerate() { + let encoding = self.tokenizer.encode(text)?; + let ids = &encoding.ids; + let offsets = &encoding.offsets; + + for start in (0..ids.len()).step_by(step) { + let end = (start + CONTENT_TOKEN_BUDGET).min(ids.len()); + let mut input_ids = Vec::with_capacity(end - start + SPECIAL_TOKEN_COUNT); + input_ids.push(self.cls_id); + input_ids.extend(ids[start..end].iter().map(|id| i64::from(*id))); + input_ids.push(self.sep_id); + + let mut window_type_ids = Vec::with_capacity(end - start + SPECIAL_TOKEN_COUNT); + window_type_ids.push(0); + window_type_ids.extend(std::iter::repeat_n(0, end - start)); + window_type_ids.push(0); + + let mut window_offsets = Vec::with_capacity(end - start + SPECIAL_TOKEN_COUNT); + window_offsets.push(None); + window_offsets.extend(offsets[start..end].iter().copied().map(Some)); + window_offsets.push(None); + windows.push(Window { + text_index, + input_ids, + token_type_ids: window_type_ids, + offsets: window_offsets, + }); + if windows.len() > self.max_windows_per_payload { + return Err(inference_error(format!( + "selected content exceeded max_windows_per_payload={}", + self.max_windows_per_payload + ))); + } + if end == ids.len() { + break; + } + } + } + Ok(windows) + } + + fn infer_batch( + &self, + windows: &[Window], + batch: &[usize], + ) -> PluginResult> { + let max_length = batch + .iter() + .map(|index| windows[*index].input_ids.len()) + .max() + .ok_or_else(|| inference_error("Rampart inference batch must not be empty"))?; + let shape = (batch.len(), max_length); + let mut input_ids = tract_ndarray::Array2::from_elem(shape, self.pad_id); + let mut attention_mask = tract_ndarray::Array2::zeros(shape); + let mut token_type_ids = tract_ndarray::Array2::zeros(shape); + for (batch_index, window_index) in batch.iter().copied().enumerate() { + let window = &windows[window_index]; + for (token_index, input_id) in window.input_ids.iter().copied().enumerate() { + input_ids[[batch_index, token_index]] = input_id; + attention_mask[[batch_index, token_index]] = 1_i64; + token_type_ids[[batch_index, token_index]] = window.token_type_ids[token_index]; + } + } + + let outputs = self + .plan + .run(tvec![ + input_ids.into_tensor().into(), + attention_mask.into_tensor().into(), + token_type_ids.into_tensor().into(), + ]) + .map_err(|error| inference_error(format!("Rampart inference failed: {error}")))?; + let output = outputs + .first() + .ok_or_else(|| inference_error("Rampart inference returned no logits"))?; + let view = output + .to_plain_array_view::() + .map_err(|error| inference_error(format!("invalid Rampart logits: {error}")))?; + let expected = [batch.len(), max_length, self.labels.len()]; + if view.shape() != expected { + return Err(inference_error(format!( + "Rampart logits shape must be {expected:?}, got {:?}", + view.shape() + ))); + } + view.to_owned() + .into_dimensionality::() + .map_err(|error| inference_error(format!("invalid Rampart logits rank: {error}"))) + } + + fn decode_window( + &self, + window: &Window, + logits: &tract_ndarray::Array3, + batch_index: usize, + ) -> PluginResult> { + let mut spans = Vec::new(); + let mut current: Option = None; + for (token_index, offset) in window.offsets.iter().copied().enumerate() { + let (label_index, score) = + max_label_and_score(logits, batch_index, token_index, self.labels.len())?; + let raw_label = &self.labels[label_index]; + let Some((prefix, label)) = split_bio_label(raw_label) else { + if let Some(span) = current.take() { + spans.push(span.finish()); + } + continue; + }; + let Some((start, end)) = offset.filter(|(start, end)| start < end) else { + if let Some(span) = current.take() { + spans.push(span.finish()); + } + continue; + }; + + let continue_span = current + .as_ref() + .is_some_and(|span| prefix == "I" && span.label == label); + if continue_span { + let span = current.as_mut().expect("checked current span"); + span.end = span.end.max(end); + span.score_total += score; + span.token_count += 1; + } else { + if let Some(span) = current.take() { + spans.push(span.finish()); + } + current = Some(SpanAccumulator { + start, + end, + label: label.to_ascii_uppercase(), + score_total: score, + token_count: 1, + }); + } + } + if let Some(span) = current { + spans.push(span.finish()); + } + Ok(spans) + } +} + +fn validate_model_inputs(model: &InferenceModel) -> PluginResult<()> { + let names = model + .input_outlets() + .map_err(|error| invalid_model(format!("invalid Rampart model inputs: {error}")))? + .iter() + .map(|outlet| model.node(outlet.node).name.as_str()) + .collect::>(); + if names != ["input_ids", "attention_mask", "token_type_ids"] { + return Err(invalid_model(format!( + "Rampart ONNX inputs must be input_ids, attention_mask, and token_type_ids; got {names:?}" + ))); + } + Ok(()) +} + +fn parse_labels(raw_labels: HashMap) -> PluginResult> { + let mut labels = raw_labels + .into_iter() + .map(|(index, label)| { + index + .parse::() + .map(|index| (index, label)) + .map_err(|_| invalid_model("Rampart label IDs must be unsigned integers")) + }) + .collect::>>()?; + labels.sort_by_key(|(index, _)| *index); + if labels.is_empty() + || labels[0].1 != "O" + || labels + .iter() + .enumerate() + .any(|(expected, (actual, _))| expected != *actual) + { + return Err(invalid_model( + "Rampart label IDs must be contiguous from zero with label 0 set to O", + )); + } + Ok(labels.into_iter().map(|(_, label)| label).collect()) +} + +fn required_token_id(tokenizer: &RampartTokenizer, token: &str) -> PluginResult { + tokenizer.token_to_id(token).map(i64::from).ok_or_else(|| { + invalid_model(format!( + "Rampart tokenizer is missing required token {token}" + )) + }) +} + +fn inference_batches(windows: &[Window], max_batch_size: usize) -> Vec> { + let mut ordered = (0..windows.len()).collect::>(); + ordered.sort_by_key(|index| windows[*index].input_ids.len()); + let mut batches = Vec::new(); + let mut batch = Vec::new(); + let mut max_tokens = 0; + for index in ordered { + let next_max_tokens = max_tokens.max(windows[index].input_ids.len()); + let padded_tokens = (batch.len() + 1) * next_max_tokens; + if !batch.is_empty() + && (batch.len() >= max_batch_size || padded_tokens > MAX_PADDED_TOKENS_PER_BATCH) + { + batches.push(std::mem::take(&mut batch)); + max_tokens = 0; + } + max_tokens = max_tokens.max(windows[index].input_ids.len()); + batch.push(index); + } + if !batch.is_empty() { + batches.push(batch); + } + batches +} + +fn max_label_and_score( + logits: &tract_ndarray::Array3, + batch_index: usize, + token_index: usize, + label_count: usize, +) -> PluginResult<(usize, f64)> { + let mut label_index = 0; + let mut maximum = f32::NEG_INFINITY; + for index in 0..label_count { + let value = logits[[batch_index, token_index, index]]; + if !value.is_finite() { + return Err(inference_error( + "Rampart logits must contain only finite values", + )); + } + if value > maximum { + maximum = value; + label_index = index; + } + } + let denominator = (0..label_count) + .map(|index| (logits[[batch_index, token_index, index]] - maximum).exp() as f64) + .sum::(); + if !denominator.is_finite() || denominator <= 0.0 { + return Err(inference_error( + "Rampart logits produced an invalid confidence score", + )); + } + Ok((label_index, 1.0 / denominator)) +} + +fn split_bio_label(label: &str) -> Option<(&str, &str)> { + if label == "O" { + return None; + } + label + .strip_prefix("B-") + .map(|label| ("B", label)) + .or_else(|| label.strip_prefix("I-").map(|label| ("I", label))) + .or(Some(("B", label))) +} + +fn merge_overlapping_spans(mut spans: Vec) -> Vec { + spans.sort_by(|left, right| { + (left.start, std::cmp::Reverse(left.end)) + .cmp(&(right.start, std::cmp::Reverse(right.end))) + .then_with(|| right.score.total_cmp(&left.score)) + .then_with(|| left.label.cmp(&right.label)) + }); + let mut merged: Vec = Vec::new(); + for span in spans { + let Some(previous) = merged.last_mut() else { + merged.push(span); + continue; + }; + if span.start > previous.end || (span.start == previous.end && span.label != previous.label) + { + merged.push(span); + continue; + } + let span_wins = (span.score, span.end - span.start, span.label.as_str()) + > ( + previous.score, + previous.end - previous.start, + previous.label.as_str(), + ); + previous.start = previous.start.min(span.start); + previous.end = previous.end.max(span.end); + previous.score = previous.score.max(span.score); + if span_wins { + previous.label = span.label; + } + } + merged +} + +fn verify_model_files(model_root: &Path) -> PluginResult { + let mut files = HashMap::new(); + for (relative_path, expected) in MODEL_FILES { + let path = model_root.join(relative_path); + files.insert( + *relative_path, + open_verified_model_file(&path, relative_path, expected)?, + ); + } + Ok(VerifiedModelFiles { + config: required_verified_file(&mut files, "config.json")?, + model: required_verified_file(&mut files, "onnx/model_q4.onnx")?, + vocab: required_verified_file(&mut files, "vocab.txt")?, + }) +} + +fn open_verified_model_file(path: &Path, display_name: &str, expected: &str) -> PluginResult { + let mut file = File::open(path).map_err(|error| { + invalid_model(format!( + "Rampart model is missing required file '{display_name}': {error}" + )) + })?; + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let read = file.read(&mut buffer).map_err(|error| { + invalid_model(format!( + "failed to read Rampart model file '{display_name}': {error}" + )) + })?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + let digest = hasher.finalize(); + let mut actual = String::with_capacity(digest.len() * 2); + for byte in digest { + write!(actual, "{byte:02x}").expect("writing to a string cannot fail"); + } + if actual != expected { + return Err(invalid_model(format!( + "Rampart model file '{display_name}' failed SHA-256 verification" + ))); + } + file.seek(SeekFrom::Start(0)).map_err(|error| { + invalid_model(format!( + "failed to rewind Rampart model file '{display_name}': {error}" + )) + })?; + Ok(file) +} + +fn required_verified_file( + files: &mut HashMap<&'static str, File>, + relative_path: &'static str, +) -> PluginResult { + files.remove(relative_path).ok_or_else(|| { + invalid_model(format!( + "Rampart integrity manifest is missing '{relative_path}'" + )) + }) +} + +fn invalid_model(message: impl Into) -> PluginError { + PluginError::InvalidConfig(message.into()) +} + +fn inference_error(message: impl Into) -> PluginError { + PluginError::Internal(message.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn batches_bound_padded_token_volume() { + let windows = (0..16) + .map(|_| Window { + text_index: 0, + input_ids: vec![0; 32], + token_type_ids: vec![0; 32], + offsets: vec![None; 32], + }) + .chain((0..2).map(|_| Window { + text_index: 0, + input_ids: vec![0; MODEL_MAX_TOKENS], + token_type_ids: vec![0; MODEL_MAX_TOKENS], + offsets: vec![None; MODEL_MAX_TOKENS], + })) + .collect::>(); + let batches = inference_batches(&windows, 16); + assert_eq!(batches.iter().map(Vec::len).collect::>(), [16, 1, 1]); + assert!(batches.iter().all(|batch| { + batch.len() + * batch + .iter() + .map(|index| windows[*index].input_ids.len()) + .max() + .unwrap() + <= MAX_PADDED_TOKENS_PER_BATCH + })); + } + + #[test] + fn overlap_merge_is_deterministic() { + let merged = merge_overlapping_spans(vec![ + Span { + start: 0, + end: 10, + label: "GIVEN_NAME".into(), + score: 0.8, + }, + Span { + start: 5, + end: 12, + label: "SURNAME".into(), + score: 0.9, + }, + Span { + start: 20, + end: 24, + label: "PHONE".into(), + score: 0.7, + }, + Span { + start: 24, + end: 28, + label: "PHONE".into(), + score: 0.8, + }, + ]); + assert_eq!(merged.len(), 2); + assert_eq!((merged[0].start, merged[0].end), (0, 12)); + assert_eq!(merged[0].label, "SURNAME"); + assert_eq!((merged[1].start, merged[1].end), (20, 28)); + } + + #[test] + fn confidence_uses_stable_softmax_and_rejects_non_finite_logits() { + let logits = tract_ndarray::Array3::from_shape_vec((1, 1, 3), vec![0.0, 2.0, 1.0]).unwrap(); + let (label, score) = max_label_and_score(&logits, 0, 0, 3).unwrap(); + let expected = 1.0 / (1.0 + (-1.0_f64).exp() + (-2.0_f64).exp()); + assert_eq!(label, 1); + assert!((score - expected).abs() < 1e-7); + + let invalid = + tract_ndarray::Array3::from_shape_vec((1, 1, 3), vec![0.0, f32::NAN, 1.0]).unwrap(); + assert!(max_label_and_score(&invalid, 0, 0, 3).is_err()); + } + + #[test] + fn model_file_verification_rejects_missing_and_modified_files() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("model.bin"); + let expected = { + let digest = Sha256::digest(b"trusted model"); + let mut value = String::with_capacity(digest.len() * 2); + for byte in digest { + write!(value, "{byte:02x}").unwrap(); + } + value + }; + + let missing = open_verified_model_file(&path, "model.bin", &expected).unwrap_err(); + assert!(missing.to_string().contains("missing required file")); + + fs::write(&path, b"trusted model").unwrap(); + open_verified_model_file(&path, "model.bin", &expected).unwrap(); + fs::write(&path, b"modified model").unwrap(); + let modified = open_verified_model_file(&path, "model.bin", &expected).unwrap_err(); + assert!(modified.to_string().contains("SHA-256 verification")); + } +} diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs new file mode 100644 index 000000000..de6c82f52 --- /dev/null +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -0,0 +1,737 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashSet; +use std::sync::Arc; + +use nemo_relay::api::event::Event; +use nemo_relay::api::llm::LlmRequest; +use nemo_relay::api::runtime::{ + BuiltinLlmCodec, EventSanitizeFn, LlmCodecIdentity, LlmSanitizeRequestFn, + LlmSanitizeResponseFn, ToolSanitizeFn, +}; +use nemo_relay::codec::resolve::{ + ProviderSurface, detect_response_surface, request_codec as build_request_codec, + response_codec as build_response_codec, +}; +use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; +use nemo_relay::plugin::{PluginError, Result as PluginResult}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use serde_json::{Map, Value as Json}; + +use crate::builtin::escape_json_pointer_segment; +use crate::overlay::BuiltinCodecName; + +use super::RampartPiiConfig; +use super::model::{Detection, RampartDetector}; + +const MAX_TEXT_BYTES: usize = 16 * 1024; +const MAX_TEXTS_PER_PAYLOAD: usize = 256; +const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; + +pub(super) trait DetectionModel: Send + Sync { + fn detect(&self, texts: &[&str]) -> PluginResult>; +} + +impl DetectionModel for RampartDetector { + fn detect(&self, texts: &[&str]) -> PluginResult> { + RampartDetector::detect(self, texts) + } +} + +#[derive(Clone)] +pub(super) struct RampartSanitizer { + detector: Arc, + target_paths: Arc>>, + target_path_patterns: Arc>, + min_score: f64, + excluded_labels: Arc>, + replacement: Arc, + legacy_surface: Option, +} + +#[derive(Clone)] +struct JsonPointerPattern { + segments: Vec, +} + +impl JsonPointerPattern { + fn compile(pattern: String) -> Self { + Self { + segments: compile_json_pointer(pattern), + } + } + + fn matches(&self, path: &[String]) -> bool { + self.segments.len() == path.len() + && self + .segments + .iter() + .zip(path) + .all(|(pattern, segment)| pattern == "*" || pattern == segment) + } +} + +struct SelectedText { + text: String, + eligible: bool, +} + +enum EventField { + Data, + CategoryProfile, + Metadata, +} + +impl RampartSanitizer { + pub(super) fn new( + config: RampartPiiConfig, + detector: Arc, + ) -> PluginResult { + let legacy_surface = match config.codec.as_deref() { + Some(codec) => Some(ProviderSurface::from_codec_name(codec).ok_or_else(|| { + PluginError::InvalidConfig(format!("unsupported Rampart PII codec '{codec}'")) + })?), + None => None, + }; + Ok(Self { + detector, + target_paths: Arc::new( + config + .target_paths + .into_iter() + .map(compile_json_pointer) + .collect(), + ), + target_path_patterns: Arc::new( + config + .target_path_patterns + .into_iter() + .map(JsonPointerPattern::compile) + .collect(), + ), + min_score: config.min_score, + excluded_labels: Arc::new(config.excluded_labels.into_iter().collect()), + replacement: config.replacement.into(), + legacy_surface, + }) + } + + fn sanitize_json(&self, value: Json) -> Json { + self.sanitize_json_values(vec![value]) + .pop() + .expect("single-value sanitization returns one value") + } + + fn sanitize_json_values(&self, values: Vec) -> Vec { + self.sanitize_json_roots( + values + .into_iter() + .map(|value| (Vec::new(), value)) + .collect(), + ) + } + + fn sanitize_json_roots(&self, mut roots: Vec<(Vec, Json)>) -> Vec { + let mut texts = Vec::new(); + let mut total_bytes = 0; + let mut within_budget = true; + for (path, value) in &roots { + let mut path = path.clone(); + self.collect_strings( + value, + &mut path, + &mut texts, + &mut total_bytes, + &mut within_budget, + ); + } + + let sanitized = self.sanitize_texts(texts); + let mut index = 0; + for (path, value) in &mut roots { + let mut path = path.clone(); + self.replace_strings(value, &mut path, &sanitized, &mut index); + } + roots.into_iter().map(|(_, value)| value).collect() + } + + fn collect_strings( + &self, + value: &Json, + path: &mut Vec, + texts: &mut Vec, + total_bytes: &mut usize, + within_budget: &mut bool, + ) { + match value { + Json::String(text) if self.matches_path(path) => { + if !*within_budget || texts.len() >= MAX_TEXTS_PER_PAYLOAD { + *within_budget = false; + return; + } + if text.len() > MAX_TEXT_BYTES { + texts.push(SelectedText { + text: self.replacement.to_string(), + eligible: false, + }); + return; + } + let Some(next_total) = total_bytes.checked_add(text.len()) else { + *within_budget = false; + return; + }; + if next_total > MAX_PAYLOAD_TEXT_BYTES { + *within_budget = false; + return; + } + *total_bytes = next_total; + texts.push(SelectedText { + text: text.clone(), + eligible: true, + }); + } + Json::Array(items) => { + for (index, item) in items.iter().enumerate() { + path.push(index.to_string()); + self.collect_strings(item, path, texts, total_bytes, within_budget); + path.pop(); + } + } + Json::Object(fields) => { + for (key, value) in fields { + path.push(escape_json_pointer_segment(key)); + self.collect_strings(value, path, texts, total_bytes, within_budget); + path.pop(); + } + } + _ => {} + } + } + + fn replace_strings( + &self, + value: &mut Json, + path: &mut Vec, + sanitized: &[String], + index: &mut usize, + ) { + match value { + Json::String(text) if self.matches_path(path) => { + *text = sanitized + .get(*index) + .cloned() + .unwrap_or_else(|| self.replacement.to_string()); + *index += 1; + } + Json::Array(items) => { + for (item_index, item) in items.iter_mut().enumerate() { + path.push(item_index.to_string()); + self.replace_strings(item, path, sanitized, index); + path.pop(); + } + } + Json::Object(fields) => { + for (key, value) in fields { + path.push(escape_json_pointer_segment(key)); + self.replace_strings(value, path, sanitized, index); + path.pop(); + } + } + _ => {} + } + } + + fn matches_path(&self, path: &[String]) -> bool { + self.target_paths.contains(path) + || self + .target_path_patterns + .iter() + .any(|pattern| pattern.matches(path)) + } + + fn sanitize_texts(&self, mut texts: Vec) -> Vec { + let eligible = texts + .iter() + .enumerate() + .filter_map(|(index, text)| text.eligible.then_some(index)) + .collect::>(); + if !eligible.is_empty() && self.sanitize_batch(&mut texts, &eligible).is_err() { + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + selected_text_count = eligible.len(), + reason = "model_or_output"; + "Rampart PII inference failed closed" + ); + for index in eligible { + texts[index].text = self.replacement.to_string(); + } + } + texts.into_iter().map(|selected| selected.text).collect() + } + + fn sanitize_batch(&self, texts: &mut [SelectedText], batch: &[usize]) -> PluginResult<()> { + let selected = batch + .iter() + .map(|index| texts[*index].text.as_str()) + .collect::>(); + let detections = self.detector.detect(&selected)?; + let mut by_text = vec![Vec::::new(); batch.len()]; + for detection in detections { + if detection.text_index >= batch.len() + || !detection.score.is_finite() + || !(0.0..=1.0).contains(&detection.score) + { + return Err(PluginError::Internal( + "Rampart returned an invalid detection".into(), + )); + } + by_text[detection.text_index].push(detection); + } + + for (original_index, mut detections) in batch.iter().copied().zip(by_text) { + detections.retain(|detection| { + detection.score >= self.min_score + && !self.excluded_labels.contains(&detection.label) + }); + if detections.is_empty() { + continue; + } + detections.sort_by_key(|detection| (detection.start_utf8, detection.end_utf8)); + let text = &texts[original_index].text; + let mut previous_end = 0; + for detection in &detections { + if detection.start_utf8 >= detection.end_utf8 + || detection.end_utf8 > text.len() + || !text.is_char_boundary(detection.start_utf8) + || !text.is_char_boundary(detection.end_utf8) + || detection.start_utf8 < previous_end + { + return Err(PluginError::Internal( + "Rampart returned invalid or overlapping UTF-8 spans".into(), + )); + } + previous_end = detection.end_utf8; + } + let mut redacted = text.clone(); + for detection in detections.iter().rev() { + redacted.replace_range( + detection.start_utf8..detection.end_utf8, + self.replacement.as_ref(), + ); + } + texts[original_index].text = redacted; + } + Ok(()) + } + + fn sanitize_request_with_codec( + &self, + codec: &dyn LlmCodec, + request: &LlmRequest, + ) -> Option { + let annotated = codec.decode(request).ok()?; + let annotated = serde_json::to_value(annotated).ok()?; + let (headers, annotated) = + self.sanitize_request_parts(request.headers.clone(), annotated)?; + let annotated = serde_json::from_value(annotated).ok()?; + let mut encoded = codec.encode(&annotated, request).ok()?; + encoded.headers = headers; + Some(encoded) + } + + fn sanitize_raw_request(&self, mut request: LlmRequest) -> Option { + let headers = std::mem::take(&mut request.headers); + let content = std::mem::take(&mut request.content); + let (headers, content) = self.sanitize_request_parts(headers, content)?; + request.headers = headers; + request.content = content; + Some(request) + } + + fn sanitize_request_parts( + &self, + headers: Map, + content: Json, + ) -> Option<(Map, Json)> { + let mut values = self.sanitize_json_roots(vec![ + (vec!["headers".to_string()], Json::Object(headers)), + (Vec::new(), content), + ]); + let content = values.pop()?; + let Json::Object(headers) = values.pop()? else { + return None; + }; + Some((headers, content)) + } + + fn sanitize_response_with_codec( + &self, + codec: &dyn LlmResponseCodec, + surface: ProviderSurface, + payload: Json, + ) -> Option { + let codec_name = BuiltinCodecName::from_provider_surface(surface); + let annotated = codec.decode_response(&payload).ok()?; + let sanitized = sanitize_serializable(self, annotated).ok()?; + Some(codec_name.overlay_response_payload(payload, &sanitized)) + } + + fn selected_surface(&self, codec: &LlmCodecIdentity) -> Option { + match codec { + LlmCodecIdentity::None => self.legacy_surface, + LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat) => { + Some(ProviderSurface::OpenAIChat) + } + LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiResponses) => { + Some(ProviderSurface::OpenAIResponses) + } + LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::AnthropicMessages) => { + Some(ProviderSurface::AnthropicMessages) + } + LlmCodecIdentity::Runtime(_) | LlmCodecIdentity::Opaque => None, + } + } + + fn uses_compatible_legacy_response_codec(&self, payload: &Json) -> bool { + self.legacy_surface + .is_some_and(|surface| detect_response_surface(payload) == Some(surface)) + } + + fn log_codec_failure(&self, direction: &'static str, codec: &LlmCodecIdentity, reason: &str) { + let codec_kind = match codec { + LlmCodecIdentity::None => "none", + LlmCodecIdentity::BuiltIn(_) => "builtin", + LlmCodecIdentity::Runtime(_) => "runtime", + LlmCodecIdentity::Opaque => "opaque", + }; + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_codec_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + direction, + codec_kind, + reason; + "Rampart PII payload omitted after codec failure" + ); + } +} + +pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeFn { + Arc::new(move |_name, payload| backend.sanitize_json(payload)) +} + +pub(super) fn event_sanitize_callback( + backend: RampartSanitizer, + scope_categories: Option<(bool, bool)>, +) -> EventSanitizeFn { + Arc::new(move |event, mut fields| { + if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { + matches!(event, Event::Scope(_)) + && event + .category() + .is_some_and(|category| match category.as_str() { + "llm" => !sanitize_llm, + "tool" => !sanitize_tool, + _ => false, + }) + }) { + return fields; + } + let specialized_scope = matches!(event, Event::Scope(_)) + && event + .category() + .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); + + let mut selected = Vec::with_capacity(3); + if !specialized_scope && let Some(data) = fields.data.take() { + selected.push((EventField::Data, data)); + } + if !specialized_scope + && let Some(profile) = fields.category_profile.take() + && let Ok(profile) = serde_json::to_value(profile) + { + selected.push((EventField::CategoryProfile, profile)); + } + if let Some(metadata) = fields.metadata.take() { + selected.push((EventField::Metadata, metadata)); + } + + let values = selected + .iter_mut() + .map(|(_, value)| std::mem::take(value)) + .collect(); + for ((field, _), value) in selected + .into_iter() + .zip(backend.sanitize_json_values(values)) + { + match field { + EventField::Data => fields.data = Some(value), + EventField::CategoryProfile => { + fields.category_profile = serde_json::from_value(value).ok(); + } + EventField::Metadata => fields.metadata = Some(value), + } + } + fields + }) +} + +pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSanitizeRequestFn { + Arc::new(move |request, context| { + if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { + return backend.sanitize_raw_request(request); + } + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + backend + .selected_surface(context.codec()) + .map(build_request_codec) + } else { + None + }; + let sanitized = resolved + .as_deref() + .or(fallback.as_deref()) + .and_then(|codec| backend.sanitize_request_with_codec(codec, &request)); + if sanitized.is_none() { + backend.log_codec_failure( + "request", + context.codec(), + "codec decode, sanitize, or encode failure", + ); + } + sanitized + }) +} + +pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSanitizeResponseFn { + Arc::new(move |payload, context| { + if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { + return Some(backend.sanitize_json(payload)); + } + if matches!(context.codec(), LlmCodecIdentity::None) + && !backend.uses_compatible_legacy_response_codec(&payload) + { + backend.log_codec_failure("response", context.codec(), "no compatible legacy codec"); + return None; + } + let surface = backend.selected_surface(context.codec()); + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + surface.map(build_response_codec) + } else { + None + }; + let sanitized = surface + .zip(resolved.as_deref().or(fallback.as_deref())) + .and_then(|(surface, codec)| { + backend.sanitize_response_with_codec(codec, surface, payload) + }); + if sanitized.is_none() { + backend.log_codec_failure( + "response", + context.codec(), + "codec decode, sanitize, or encode failure", + ); + } + sanitized + }) +} + +fn compile_json_pointer(pointer: String) -> Vec { + pointer.strip_prefix('/').map_or_else(Vec::new, |path| { + path.split('/').map(str::to_string).collect() + }) +} + +fn sanitize_serializable(backend: &RampartSanitizer, value: T) -> PluginResult +where + T: Serialize + DeserializeOwned, +{ + let value = serde_json::to_value(value)?; + serde_json::from_value(backend.sanitize_json(value)).map_err(PluginError::from) +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + + use super::*; + + struct NameDetector; + + impl DetectionModel for NameDetector { + fn detect(&self, texts: &[&str]) -> PluginResult> { + Ok(texts + .iter() + .enumerate() + .filter_map(|(text_index, text)| { + text.find("José").map(|start| Detection { + text_index, + start_utf8: start, + end_utf8: start + "José".len(), + label: "GIVEN_NAME".into(), + score: 0.99, + }) + }) + .collect()) + } + } + + struct FailingDetector; + + impl DetectionModel for FailingDetector { + fn detect(&self, _texts: &[&str]) -> PluginResult> { + Err(PluginError::Internal("model failure".into())) + } + } + + struct CountingDetector(Arc); + + impl DetectionModel for CountingDetector { + fn detect(&self, _texts: &[&str]) -> PluginResult> { + self.0.fetch_add(1, Ordering::Relaxed); + Ok(Vec::new()) + } + } + + fn sanitizer(detector: Arc, patterns: Vec<&str>) -> RampartSanitizer { + RampartSanitizer::new( + RampartPiiConfig { + model_path: "/tmp/rampart".into(), + target_path_patterns: patterns.into_iter().map(str::to_string).collect(), + ..RampartPiiConfig::default() + }, + detector, + ) + .unwrap() + } + + #[test] + fn sanitizes_selected_utf8_spans_without_touching_metadata() { + let sanitizer = sanitizer( + Arc::new(NameDetector), + vec!["/messages/*/content", "/message"], + ); + let value = serde_json::json!({ + "messages": [{"content": "Hello José Rivera"}], + "message": "José", + "model": "model-José" + }); + assert_eq!( + sanitizer.sanitize_json(value), + serde_json::json!({ + "messages": [{"content": "Hello [REDACTED] Rivera"}], + "message": "[REDACTED]", + "model": "model-José" + }) + ); + } + + #[test] + fn model_errors_fail_closed_only_for_selected_values() { + let sanitizer = sanitizer(Arc::new(FailingDetector), vec!["/message"]); + assert_eq!( + sanitizer.sanitize_json(serde_json::json!({ + "message": "private", + "metadata": "visible" + })), + serde_json::json!({ + "message": "[REDACTED]", + "metadata": "visible" + }) + ); + } + + #[test] + fn selected_values_over_payload_budget_fail_closed() { + let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/*"]); + let value = Json::Object( + (0..=MAX_TEXTS_PER_PAYLOAD) + .map(|index| (index.to_string(), Json::String("safe".into()))) + .collect(), + ); + let sanitized = sanitizer.sanitize_json(value); + assert_eq!( + sanitized + .as_object() + .unwrap() + .values() + .filter(|value| **value == "[REDACTED]") + .count(), + 1 + ); + } + + #[test] + fn selected_payload_uses_one_detector_call() { + let calls = Arc::new(AtomicUsize::new(0)); + let sanitizer = sanitizer(Arc::new(CountingDetector(Arc::clone(&calls))), vec!["/*"]); + let value = Json::Object( + (0..128) + .map(|index| (index.to_string(), Json::String("safe".into()))) + .collect(), + ); + assert_eq!( + sanitizer.sanitize_json(value).as_object().unwrap().len(), + 128 + ); + assert_eq!(calls.load(Ordering::Relaxed), 1); + } + + #[test] + fn openai_chat_request_projection_preserves_provider_fields() { + let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/messages/*/content"]); + let request = LlmRequest { + headers: Map::from_iter([("x-vendor".into(), Json::String("José-header".into()))]), + content: serde_json::json!({ + "model": "model-José", + "messages": [{"role": "user", "content": "Hello José"}], + "vendor_trace": "trace-José" + }), + }; + let codec = build_request_codec(ProviderSurface::OpenAIChat); + let sanitized = sanitizer + .sanitize_request_with_codec(codec.as_ref(), &request) + .unwrap(); + + assert_eq!( + sanitized.content["messages"][0]["content"], + "Hello [REDACTED]" + ); + assert_eq!(sanitized.content["model"], "model-José"); + assert_eq!(sanitized.content["vendor_trace"], "trace-José"); + assert_eq!(sanitized.headers["x-vendor"], "José-header"); + } + + #[test] + fn openai_chat_response_projection_preserves_provider_fields() { + let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/message"]); + let payload = serde_json::json!({ + "id": "chatcmpl-José", + "model": "model-José", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": "Hello José"}, + "finish_reason": "stop" + }], + "vendor_trace": "trace-José" + }); + let codec = build_response_codec(ProviderSurface::OpenAIChat); + let sanitized = sanitizer + .sanitize_response_with_codec(codec.as_ref(), ProviderSurface::OpenAIChat, payload) + .unwrap(); + + assert_eq!( + sanitized["choices"][0]["message"]["content"], + "Hello [REDACTED]" + ); + assert_eq!(sanitized["id"], "chatcmpl-José"); + assert_eq!(sanitized["model"], "model-José"); + assert_eq!(sanitized["vendor_trace"], "trace-José"); + } +} diff --git a/crates/pii-redaction/src/rampart/tokenizer.rs b/crates/pii-redaction/src/rampart/tokenizer.rs new file mode 100644 index 000000000..6224038d7 --- /dev/null +++ b/crates/pii-redaction/src/rampart/tokenizer.rs @@ -0,0 +1,333 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::io::BufRead; + +use nemo_relay::plugin::{PluginError, Result as PluginResult}; +use unicode_categories::UnicodeCategories; +use unicode_normalization::UnicodeNormalization; + +const UNKNOWN_TOKEN: &str = "[UNK]"; +const CONTINUATION_PREFIX: &str = "##"; +const MAX_CHARS_PER_WORD: usize = 100; +const SPECIAL_TOKENS: &[&str] = &["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]; + +pub(super) struct EncodedText { + pub(super) ids: Vec, + pub(super) offsets: Vec<(usize, usize)>, +} + +pub(super) struct RampartTokenizer { + vocab: HashMap, + unknown_id: u32, +} + +#[derive(Clone, Copy)] +struct MappedChar { + value: char, + original_start: usize, + original_end: usize, + isolate: bool, +} + +struct Piece { + id: u32, + start: usize, + end: usize, +} + +impl RampartTokenizer { + pub(super) fn from_vocab_reader(reader: impl BufRead) -> PluginResult { + let mut vocab = HashMap::new(); + for (index, line) in reader.lines().enumerate() { + let line = line.map_err(|error| { + invalid_tokenizer(format!("failed to read Rampart vocab.txt: {error}")) + })?; + let token = line.strip_suffix('\r').unwrap_or(&line).to_string(); + let id = u32::try_from(index) + .map_err(|_| invalid_tokenizer("Rampart vocabulary is too large"))?; + if vocab.insert(token, id).is_some() { + return Err(invalid_tokenizer( + "Rampart vocabulary contains duplicate tokens", + )); + } + } + let unknown_id = vocab + .get(UNKNOWN_TOKEN) + .copied() + .ok_or_else(|| invalid_tokenizer("Rampart vocabulary is missing [UNK]"))?; + Ok(Self { vocab, unknown_id }) + } + + pub(super) fn token_to_id(&self, token: &str) -> Option { + self.vocab.get(token).copied() + } + + pub(super) fn encode(&self, text: &str) -> PluginResult { + let mut ids = Vec::new(); + let mut offsets = Vec::new(); + let mut cursor = 0; + while cursor < text.len() { + let Some((special_start, special)) = next_special_token(text, cursor) else { + self.encode_segment(&text[cursor..], cursor, &mut ids, &mut offsets)?; + break; + }; + self.encode_segment(&text[cursor..special_start], cursor, &mut ids, &mut offsets)?; + let special_end = special_start + special.len(); + ids.push(self.token_to_id(special).ok_or_else(|| { + invalid_tokenizer("Rampart vocabulary is missing a special token") + })?); + offsets.push((special_start, special_end)); + cursor = special_end; + } + Ok(EncodedText { ids, offsets }) + } + + fn encode_segment( + &self, + text: &str, + base_offset: usize, + ids: &mut Vec, + offsets: &mut Vec<(usize, usize)>, + ) -> PluginResult<()> { + for token in basic_tokens(text, base_offset) { + for piece in self.wordpiece(&token)? { + ids.push(piece.id); + offsets.push(original_offsets(&token, piece.start, piece.end)?); + } + } + Ok(()) + } + + fn wordpiece(&self, token: &[MappedChar]) -> PluginResult> { + let normalized = token.iter().map(|item| item.value).collect::(); + if token.len() > MAX_CHARS_PER_WORD { + return Ok(vec![Piece { + id: self.unknown_id, + start: 0, + end: normalized.len(), + }]); + } + + let mut pieces = Vec::new(); + let mut start = 0; + while start < normalized.len() { + let mut end = normalized.len(); + let mut matched = None; + while start < end { + let candidate = if start == 0 { + &normalized[start..end] + } else { + let mut value = String::with_capacity(CONTINUATION_PREFIX.len() + end - start); + value.push_str(CONTINUATION_PREFIX); + value.push_str(&normalized[start..end]); + if let Some(id) = self.vocab.get(&value).copied() { + matched = Some(Piece { id, start, end }); + break; + } + end -= normalized[start..end] + .chars() + .next_back() + .expect("non-empty WordPiece candidate") + .len_utf8(); + continue; + }; + if let Some(id) = self.vocab.get(candidate).copied() { + matched = Some(Piece { id, start, end }); + break; + } + end -= candidate + .chars() + .next_back() + .expect("non-empty WordPiece candidate") + .len_utf8(); + } + let Some(piece) = matched else { + return Ok(vec![Piece { + id: self.unknown_id, + start: 0, + end: normalized.len(), + }]); + }; + start = piece.end; + pieces.push(piece); + } + Ok(pieces) + } +} + +fn basic_tokens(text: &str, base_offset: usize) -> Vec> { + let mut tokens = Vec::new(); + let mut current = Vec::new(); + for item in normalize(text, base_offset) { + if item.value.is_whitespace() { + push_current(&mut tokens, &mut current); + } else if item.isolate || item.value.is_ascii_punctuation() || item.value.is_punctuation() { + push_current(&mut tokens, &mut current); + tokens.push(vec![item]); + } else { + current.push(item); + } + } + push_current(&mut tokens, &mut current); + tokens +} + +fn push_current(tokens: &mut Vec>, current: &mut Vec) { + if !current.is_empty() { + tokens.push(std::mem::take(current)); + } +} + +fn normalize(text: &str, base_offset: usize) -> Vec { + let mut normalized = Vec::with_capacity(text.chars().count()); + for (relative_start, original) in text.char_indices() { + let original_start = base_offset + relative_start; + let original_end = original_start + original.len_utf8(); + // Rampart's model card recommends splitting hyphenated identifiers. + // Replacing one ASCII byte preserves the original byte offsets. + let cleaned = if original == '-' || is_whitespace(original) { + ' ' + } else if original == '\0' || original == '\u{fffd}' || original.is_other() { + continue; + } else { + original + }; + let isolate = is_chinese_char(cleaned); + for decomposed in std::iter::once(cleaned).nfd() { + if decomposed.is_mark_nonspacing() { + continue; + } + for value in decomposed.to_lowercase() { + normalized.push(MappedChar { + value, + original_start, + original_end, + isolate, + }); + } + } + } + normalized +} + +fn next_special_token(text: &str, cursor: usize) -> Option<(usize, &'static str)> { + SPECIAL_TOKENS + .iter() + .filter_map(|token| { + text[cursor..] + .find(token) + .map(|relative| (cursor + relative, *token)) + }) + .min_by_key(|(start, token)| (*start, std::cmp::Reverse(token.len()))) +} + +fn original_offsets( + token: &[MappedChar], + normalized_start: usize, + normalized_end: usize, +) -> PluginResult<(usize, usize)> { + let mut cursor = 0; + let mut original_start = None; + let mut original_end = None; + for item in token { + let next = cursor + item.value.len_utf8(); + if next > normalized_start && cursor < normalized_end { + original_start.get_or_insert(item.original_start); + original_end = Some(item.original_end); + } + cursor = next; + } + original_start + .zip(original_end) + .ok_or_else(|| PluginError::Internal("Rampart tokenizer returned an invalid offset".into())) +} + +fn is_whitespace(value: char) -> bool { + matches!(value, '\t' | '\n' | '\r') || value.is_whitespace() +} + +fn is_chinese_char(value: char) -> bool { + matches!( + value as u32, + 0x4E00..=0x9FFF + | 0x3400..=0x4DBF + | 0x20000..=0x2A6DF + | 0x2A700..=0x2B73F + | 0x2B740..=0x2B81F + | 0x2B920..=0x2CEAF + | 0xF900..=0xFAFF + | 0x2F800..=0x2FA1F + ) +} + +fn invalid_tokenizer(message: impl Into) -> PluginError { + PluginError::InvalidConfig(message.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tokenizer() -> RampartTokenizer { + let tokens = [ + "[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]", "hello", ",", "jose", "##ph", "野", + "alice", "rivera", + ]; + let vocab = tokens + .into_iter() + .enumerate() + .map(|(index, token)| (token.to_string(), index as u32)) + .collect(); + RampartTokenizer { + vocab, + unknown_id: 1, + } + } + + #[test] + fn normalizes_bert_text_and_preserves_original_utf8_offsets() { + let encoded = tokenizer().encode("Héllo, JOSEPH 野").unwrap(); + assert_eq!(encoded.ids, [5, 6, 7, 8, 9]); + assert_eq!( + encoded.offsets, + [(0, 6), (6, 7), (8, 12), (12, 14), (15, 18)] + ); + } + + #[test] + fn splits_hyphens_without_shifting_offsets() { + let encoded = tokenizer().encode("Alice-Rivera").unwrap(); + assert_eq!(encoded.ids, [10, 11]); + assert_eq!(encoded.offsets, [(0, 5), (6, 12)]); + } + + #[test] + fn unknown_word_covers_the_complete_original_token() { + let encoded = tokenizer().encode("unlisted").unwrap(); + assert_eq!(encoded.ids, [1]); + assert_eq!(encoded.offsets, [(0, 8)]); + } + + #[test] + fn preserves_exact_special_tokens_before_normalization() { + let encoded = tokenizer().encode("Héllo [CLS] JOSEPH").unwrap(); + assert_eq!(encoded.ids, [5, 2, 7, 8]); + assert_eq!(encoded.offsets, [(0, 6), (7, 12), (13, 17), (17, 19)]); + } + + #[test] + fn removes_controls_and_combining_marks_without_shifting_original_offsets() { + let encoded = tokenizer().encode("Alice\0Cafe\u{301} Rivera").unwrap(); + assert_eq!(encoded.ids, [1, 11]); + assert_eq!(encoded.offsets, [(0, 10), (13, 19)]); + } + + #[test] + fn isolates_unicode_punctuation_and_chinese_characters() { + let encoded = tokenizer().encode("Alice’野").unwrap(); + assert_eq!(encoded.ids, [10, 1, 9]); + assert_eq!(encoded.offsets, [(0, 5), (5, 8), (8, 11)]); + } +} diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index ca103568b..24be61cdf 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -23,6 +23,7 @@ use nemo_relay::shared_runtime::initialize_shared_runtime_binding; use nemo_relay_adaptive::plugin_component::register_adaptive_component; use nemo_relay_pii_redaction::component::register_pii_redaction_component; +use nemo_relay_pii_redaction::rampart::register_rampart_pii_component; use pyo3::prelude::*; use pyo3::types::PyModule; @@ -59,6 +60,11 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> { "failed to register PII redaction plugin component: {e}" )) })?; + register_rampart_pii_component().map_err(|e| { + pyo3::exceptions::PyRuntimeError::new_err(format!( + "failed to register Rampart PII plugin component: {e}" + )) + })?; py_types::register(m)?; py_api::register(m)?; py_plugin::register(m)?; diff --git a/go/nemo_relay/pii_rampart.go b/go/nemo_relay/pii_rampart.go new file mode 100644 index 000000000..20580d58c --- /dev/null +++ b/go/nemo_relay/pii_rampart.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package nemo_relay + +// RampartPiiPluginKind is the in-process Rampart PII component kind. +const RampartPiiPluginKind = "pii_rampart" + +// RampartModelID is the pinned Hugging Face model repository. +const RampartModelID = "nationaldesignstudio/rampart" + +// RampartModelRevision is the pinned model revision accepted by Relay. +const RampartModelRevision = "b1993e4e68b082835b80ffc65acc03325ea2e501" + +// RampartPiiConfig configures in-process Rampart PII redaction. +type RampartPiiConfig struct { + Version uint32 `json:"version"` + ModelPath string `json:"model_path"` + Input bool `json:"input"` + Output bool `json:"output"` + Mark bool `json:"mark"` + ToolInput bool `json:"tool_input"` + ToolOutput bool `json:"tool_output"` + Priority int32 `json:"priority"` + Codec string `json:"codec,omitempty"` + TargetPaths []string `json:"target_paths,omitempty"` + TargetPathPatterns []string `json:"target_path_patterns,omitempty"` + MinScore float64 `json:"min_score"` + ExcludedLabels []string `json:"excluded_labels,omitempty"` + Replacement string `json:"replacement"` + MaxWindowsPerPayload int32 `json:"max_windows_per_payload"` + InferenceBatchSize int32 `json:"inference_batch_size"` + Policy *ConfigPolicy `json:"policy,omitempty"` +} + +// NewRampartPiiConfig returns Rampart PII settings with runtime defaults. +func NewRampartPiiConfig(modelPath string) RampartPiiConfig { + return RampartPiiConfig{ + Version: 1, + ModelPath: modelPath, + Input: true, + Output: true, + Mark: true, + ToolInput: true, + ToolOutput: true, + Priority: 100, + TargetPaths: []string{}, + TargetPathPatterns: []string{}, + MinScore: 0.4, + ExcludedLabels: []string{}, + Replacement: "[REDACTED]", + MaxWindowsPerPayload: 128, + InferenceBatchSize: 16, + } +} + +// RampartPiiComponent converts config into the shared plugin component. +func RampartPiiComponent(config RampartPiiConfig) PluginComponentSpec { + return PluginComponentSpec{ + Kind: RampartPiiPluginKind, + Enabled: true, + Config: mustConfigMap(config), + } +} + +// ValidateRampartPiiConfig validates config without loading model files. +func ValidateRampartPiiConfig(config RampartPiiConfig) (ConfigReport, error) { + return ValidatePluginConfig(PluginConfig{ + Version: 1, + Components: []PluginComponentSpec{RampartPiiComponent(config)}, + }) +} diff --git a/go/nemo_relay/pii_rampart/pii_rampart.go b/go/nemo_relay/pii_rampart/pii_rampart.go new file mode 100644 index 000000000..24920fc93 --- /dev/null +++ b/go/nemo_relay/pii_rampart/pii_rampart.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package pii_rampart + +import nemo_relay "github.com/NVIDIA/NeMo-Relay/go/nemo_relay" + +type Config = nemo_relay.RampartPiiConfig +type ConfigPolicy = nemo_relay.ConfigPolicy +type ConfigReport = nemo_relay.ConfigReport + +// PluginKind is the Rampart PII component kind. +const PluginKind = nemo_relay.RampartPiiPluginKind + +// ModelID is the pinned Hugging Face model repository. +const ModelID = nemo_relay.RampartModelID + +// ModelRevision is the pinned model revision accepted by Relay. +const ModelRevision = nemo_relay.RampartModelRevision + +// NewConfig returns Rampart PII settings with runtime defaults. +func NewConfig(modelPath string) Config { + return nemo_relay.NewRampartPiiConfig(modelPath) +} + +// Component converts config into the shared plugin component. +func Component(config Config) nemo_relay.PluginComponentSpec { + return nemo_relay.RampartPiiComponent(config) +} + +// ValidateConfig validates config without loading model files. +func ValidateConfig(config Config) (ConfigReport, error) { + return nemo_relay.ValidateRampartPiiConfig(config) +} diff --git a/go/nemo_relay/pii_rampart/pii_rampart_test.go b/go/nemo_relay/pii_rampart/pii_rampart_test.go new file mode 100644 index 000000000..5d342cb0f --- /dev/null +++ b/go/nemo_relay/pii_rampart/pii_rampart_test.go @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package pii_rampart + +import "testing" + +func TestConfigAndComponentHelpers(t *testing.T) { + config := NewConfig("/models/rampart") + config.TargetPathPatterns = []string{"/messages/*/content"} + component := Component(config) + if component.Kind != PluginKind || !component.Enabled { + t.Fatalf("unexpected Rampart PII component: %#v", component) + } + if component.Config["model_path"] != "/models/rampart" { + t.Fatalf("unexpected Rampart PII config: %#v", component.Config) + } + if ModelID != "nationaldesignstudio/rampart" || + ModelRevision != "b1993e4e68b082835b80ffc65acc03325ea2e501" { + t.Fatalf("unexpected Rampart model identity: %s@%s", ModelID, ModelRevision) + } +} diff --git a/go/nemo_relay/pii_rampart_test.go b/go/nemo_relay/pii_rampart_test.go new file mode 100644 index 000000000..07012ec13 --- /dev/null +++ b/go/nemo_relay/pii_rampart_test.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package nemo_relay + +import ( + "encoding/json" + "testing" +) + +func TestRampartPiiConfigHelpers(t *testing.T) { + config := NewRampartPiiConfig("/models/rampart") + config.TargetPathPatterns = []string{"/messages/*/content"} + component := RampartPiiComponent(config) + if component.Kind != RampartPiiPluginKind || !component.Enabled { + t.Fatalf("unexpected Rampart PII component: %#v", component) + } + if component.Config["model_path"] != "/models/rampart" { + t.Fatalf("unexpected Rampart PII config: %#v", component.Config) + } + if RampartModelID != "nationaldesignstudio/rampart" || + RampartModelRevision != "b1993e4e68b082835b80ffc65acc03325ea2e501" { + t.Fatalf("unexpected Rampart model identity: %s@%s", RampartModelID, RampartModelRevision) + } +} + +func TestRampartPiiConfigPreservesExplicitZeroValues(t *testing.T) { + config := NewRampartPiiConfig("/models/rampart") + config.Version = 0 + config.Priority = 0 + + serialized, err := json.Marshal(config) + if err != nil { + t.Fatalf("marshal Rampart PII config: %v", err) + } + var value map[string]any + if err := json.Unmarshal(serialized, &value); err != nil { + t.Fatalf("decode Rampart PII config: %v", err) + } + if value["version"] != float64(0) || value["priority"] != float64(0) { + t.Fatalf("explicit zero values were not preserved: %#v", value) + } +} diff --git a/python/nemo_relay/__init__.py b/python/nemo_relay/__init__.py index fb8c89152..1f41fc394 100644 --- a/python/nemo_relay/__init__.py +++ b/python/nemo_relay/__init__.py @@ -17,6 +17,7 @@ - ``nemo_relay.adaptive`` for adaptive component configuration helpers - ``nemo_relay.observability`` for observability component configuration helpers - ``nemo_relay.pii_redaction`` for PII redaction component configuration helpers +- ``nemo_relay.pii_rampart`` for in-process Rampart PII component helpers - ``nemo_relay.model_pricing`` for model pricing component configuration helpers Top-level exports also include: @@ -213,6 +214,7 @@ class EventSanitizeFields(TypedDict): llm, model_pricing, observability, + pii_rampart, pii_redaction, plugin, scope, @@ -458,6 +460,7 @@ def worker() -> None: "adaptive", "observability", "pii_redaction", + "pii_rampart", "model_pricing", # Scope stack isolation "ScopeStack", diff --git a/python/nemo_relay/__init__.pyi b/python/nemo_relay/__init__.pyi index 7c2372a57..fbdb53fff 100644 --- a/python/nemo_relay/__init__.pyi +++ b/python/nemo_relay/__init__.pyi @@ -32,6 +32,7 @@ from nemo_relay import intercepts as intercepts from nemo_relay import llm as llm from nemo_relay import model_pricing as model_pricing from nemo_relay import observability as observability +from nemo_relay import pii_rampart as pii_rampart from nemo_relay import pii_redaction as pii_redaction from nemo_relay import plugin as plugin from nemo_relay import scope as scope diff --git a/python/nemo_relay/pii_rampart.py b/python/nemo_relay/pii_rampart.py new file mode 100644 index 000000000..a733e0bb6 --- /dev/null +++ b/python/nemo_relay/pii_rampart.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""In-process Rampart PII plugin configuration helpers.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Literal, TypedDict, cast + +from nemo_relay import JsonObject +from nemo_relay import plugin as plugin_module +from nemo_relay.plugin import ConfigDiagnostic, ConfigPolicy + + +class ConfigReport(TypedDict): + """Validation report for Rampart PII configuration.""" + + diagnostics: list[ConfigDiagnostic] + + +@dataclass(slots=True) +class RampartPiiConfig: + """Canonical config for in-process Rampart PII redaction.""" + + model_path: str + version: int = 1 + input: bool = True + output: bool = True + mark: bool = True + tool_input: bool = True + tool_output: bool = True + priority: int = 100 + codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = None + target_paths: list[str] = field(default_factory=list) + target_path_patterns: list[str] = field(default_factory=list) + min_score: float = 0.4 + excluded_labels: list[str] = field(default_factory=list) + replacement: str = "[REDACTED]" + max_windows_per_payload: int = 128 + inference_batch_size: int = 16 + policy: ConfigPolicy = field(default_factory=ConfigPolicy) + + def to_dict(self) -> JsonObject: + """Serialize this config to the canonical JSON object shape.""" + value: JsonObject = { + "version": self.version, + "model_path": self.model_path, + "input": self.input, + "output": self.output, + "mark": self.mark, + "tool_input": self.tool_input, + "tool_output": self.tool_output, + "priority": self.priority, + "target_paths": self.target_paths, + "target_path_patterns": self.target_path_patterns, + "min_score": self.min_score, + "excluded_labels": self.excluded_labels, + "replacement": self.replacement, + "max_windows_per_payload": self.max_windows_per_payload, + "inference_batch_size": self.inference_batch_size, + "policy": self.policy.to_dict(), + } + if self.codec is not None: + value["codec"] = self.codec + return value + + +RAMPART_PII_PLUGIN_KIND = "pii_rampart" +RAMPART_MODEL_ID = "nationaldesignstudio/rampart" +RAMPART_MODEL_REVISION = "b1993e4e68b082835b80ffc65acc03325ea2e501" + + +@dataclass(slots=True) +class ComponentSpec: + """Top-level Rampart PII component wrapper.""" + + config: RampartPiiConfig | JsonObject + enabled: bool = True + + def to_dict(self) -> JsonObject: + """Serialize this component to the canonical plugin shape.""" + config = self.config.to_dict() if isinstance(self.config, RampartPiiConfig) else self.config + return { + "kind": RAMPART_PII_PLUGIN_KIND, + "enabled": self.enabled, + "config": config, + } + + +def validate_config(config: RampartPiiConfig | JsonObject) -> ConfigReport: + """Validate Rampart PII configuration without loading the model.""" + report = plugin_module.validate(plugin_module.PluginConfig(components=[ComponentSpec(config)])) + return cast(ConfigReport, report) + + +__all__ = [ + "ComponentSpec", + "ConfigReport", + "RAMPART_MODEL_ID", + "RAMPART_MODEL_REVISION", + "RAMPART_PII_PLUGIN_KIND", + "RampartPiiConfig", + "validate_config", +] diff --git a/python/nemo_relay/pii_rampart.pyi b/python/nemo_relay/pii_rampart.pyi new file mode 100644 index 000000000..a2755f8a9 --- /dev/null +++ b/python/nemo_relay/pii_rampart.pyi @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Type stubs for ``nemo_relay.pii_rampart``.""" + +from dataclasses import dataclass, field +from typing import Literal, TypedDict + +from nemo_relay import JsonObject +from nemo_relay.plugin import ConfigDiagnostic, ConfigPolicy + +class ConfigReport(TypedDict): + diagnostics: list[ConfigDiagnostic] + +@dataclass(slots=True) +class RampartPiiConfig: + model_path: str + version: int = ... + input: bool = ... + output: bool = ... + mark: bool = ... + tool_input: bool = ... + tool_output: bool = ... + priority: int = ... + codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = ... + target_paths: list[str] = field(default_factory=list) + target_path_patterns: list[str] = field(default_factory=list) + min_score: float = ... + excluded_labels: list[str] = field(default_factory=list) + replacement: str = ... + max_windows_per_payload: int = ... + inference_batch_size: int = ... + policy: ConfigPolicy = field(default_factory=ConfigPolicy) + def to_dict(self) -> JsonObject: ... + +RAMPART_PII_PLUGIN_KIND: Literal["pii_rampart"] +RAMPART_MODEL_ID: Literal["nationaldesignstudio/rampart"] +RAMPART_MODEL_REVISION: Literal["b1993e4e68b082835b80ffc65acc03325ea2e501"] + +@dataclass(slots=True) +class ComponentSpec: + config: RampartPiiConfig | JsonObject + enabled: bool = ... + def to_dict(self) -> JsonObject: ... + +def validate_config(config: RampartPiiConfig | JsonObject) -> ConfigReport: ... diff --git a/python/tests/test_pii_rampart_plugin.py b/python/tests/test_pii_rampart_plugin.py new file mode 100644 index 000000000..eafbcc4dc --- /dev/null +++ b/python/tests/test_pii_rampart_plugin.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from nemo_relay import plugin +from nemo_relay.pii_rampart import ( + RAMPART_MODEL_ID, + RAMPART_MODEL_REVISION, + RAMPART_PII_PLUGIN_KIND, + ComponentSpec, + RampartPiiConfig, + validate_config, +) + + +def test_rampart_config_and_component_shape() -> None: + config = RampartPiiConfig( + model_path="/models/rampart", + codec="openai_chat", + target_path_patterns=["/messages/*/content"], + ) + value = config.to_dict() + assert value["model_path"] == "/models/rampart" + assert value["inference_batch_size"] == 16 + assert RAMPART_MODEL_ID == "nationaldesignstudio/rampart" + assert RAMPART_MODEL_REVISION == "b1993e4e68b082835b80ffc65acc03325ea2e501" + component = ComponentSpec(config).to_dict() + assert component["kind"] == RAMPART_PII_PLUGIN_KIND + assert component["enabled"] is True + + +def test_rampart_validation_and_discovery() -> None: + report = validate_config( + RampartPiiConfig( + model_path="relative/model", + target_path_patterns=["/messages/pre*fix/content"], + ) + ) + assert {diagnostic.get("field") for diagnostic in report["diagnostics"]} == { + "model_path", + "target_path_patterns", + } + assert RAMPART_PII_PLUGIN_KIND in plugin.list_kinds() From c17ef9093bd2dfdb820dd8c86e4c1751c284d3d1 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 27 Jul 2026 14:52:26 -0700 Subject: [PATCH 12/83] style(pii): sort Rampart Python exports Signed-off-by: Alex Fournier --- python/nemo_relay/pii_rampart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/nemo_relay/pii_rampart.py b/python/nemo_relay/pii_rampart.py index a733e0bb6..a6b2a620b 100644 --- a/python/nemo_relay/pii_rampart.py +++ b/python/nemo_relay/pii_rampart.py @@ -95,11 +95,11 @@ def validate_config(config: RampartPiiConfig | JsonObject) -> ConfigReport: __all__ = [ - "ComponentSpec", - "ConfigReport", "RAMPART_MODEL_ID", "RAMPART_MODEL_REVISION", "RAMPART_PII_PLUGIN_KIND", + "ComponentSpec", + "ConfigReport", "RampartPiiConfig", "validate_config", ] From 647ab4761b3421bcc436f123cbfe9a649142cbb6 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 27 Jul 2026 18:06:40 -0700 Subject: [PATCH 13/83] chore(repo): mark generated dependency files Signed-off-by: Alex Fournier --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index b60c7cf49..dbeedad6f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 examples/python-grpc-worker-plugin/nemo_relay_python_grpc_worker_example/worker.py text eol=lf + +Cargo.lock linguist-generated=true +ATTRIBUTIONS-*.md linguist-generated=true From de4e6756895979ff89072c3d48d1d1447b8743a4 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 14:00:50 -0400 Subject: [PATCH 14/83] feat!: make middleware async across primary bindings Signed-off-by: Will Killian --- crates/adaptive/src/acg_component.rs | 46 +- .../adaptive/src/adaptive_hints_intercept.rs | 49 +- crates/adaptive/src/lib.rs | 4 + .../integration/runtime_integration_tests.rs | 12 +- crates/adaptive/tests/support/mod.rs | 12 + .../tests/unit/acg_component_tests.rs | 13 +- .../unit/adaptive_hints_intercept_tests.rs | 19 +- .../tests/unit/plugin_component_tests.rs | 1 + .../tests/unit/runtime_features_tests.rs | 25 +- crates/adaptive/tests/unit/runtime_tests.rs | 1 + crates/cli/src/sessions/mod.rs | 26 +- .../cli/tests/coverage/shared/server_tests.rs | 122 ++- crates/core/src/api/llm.rs | 465 +++++++-- crates/core/src/api/runtime/callbacks.rs | 54 +- crates/core/src/api/runtime/state.rs | 123 ++- .../src/api/runtime/subscriber_dispatcher.rs | 198 +++- crates/core/src/api/scope.rs | 33 +- crates/core/src/api/shared.rs | 58 +- crates/core/src/api/tool.rs | 229 ++++- crates/core/src/logging/rotation.rs | 4 + crates/core/src/plugin/dynamic/native.rs | 969 +++++++++++++++--- crates/core/src/plugin/dynamic/worker.rs | 172 ++-- crates/core/src/stream.rs | 233 +++-- .../tests/coverage/logging_rotation_tests.rs | 34 + .../core/tests/coverage/logging_sink_tests.rs | 67 +- .../tests/fixtures/native_plugin/src/lib.rs | 291 +++++- .../tests/integration/api_surface_tests.rs | 228 +++-- .../tests/integration/middleware_tests.rs | 383 ++++--- .../tests/integration/native_plugin_tests.rs | 128 ++- .../core/tests/integration/pipeline_tests.rs | 55 +- .../tests/integration/scope_local_tests.rs | 37 +- .../subscriber_dispatcher_tests.rs | 173 ++-- crates/core/tests/integration/test_support.rs | 18 + .../tests/integration/worker_plugin_tests.rs | 6 + crates/core/tests/unit/context_tests.rs | 25 +- .../core/tests/unit/dynamic_worker_tests.rs | 36 +- crates/core/tests/unit/llm_api_tests.rs | 33 +- crates/core/tests/unit/native_plugin_tests.rs | 127 ++- crates/core/tests/unit/plugin_tests.rs | 267 +++-- crates/core/tests/unit/shared_tests.rs | 67 +- crates/ffi/src/api/mod.rs | 17 +- crates/ffi/src/callable.rs | 351 ++++--- .../tests/integration/callable_extra_tests.rs | 61 +- crates/ffi/tests/unit/callable_tests.rs | 93 +- crates/node/src/api/mod.rs | 333 +++--- crates/node/src/callable.rs | 816 ++++++++++----- crates/node/src/callback_factory.rs | 18 +- crates/node/src/promise_call.rs | 67 +- crates/node/tests/callback_error_tests.mjs | 4 +- crates/node/tests/event_sanitizers_tests.mjs | 50 +- crates/node/tests/llm_tests.mjs | 102 +- crates/node/tests/scope_tests.mjs | 24 +- crates/node/tests/tools_tests.mjs | 62 ++ crates/pii-redaction/src/builtin.rs | 242 ++--- .../tests/unit/component_tests.rs | 192 ++-- crates/plugin/README.md | 8 +- crates/plugin/src/lib.rs | 178 +++- crates/plugin/tests/typed_callbacks.rs | 4 +- crates/python/src/py_api/mod.rs | 146 ++- crates/python/src/py_callable.rs | 434 ++++---- .../python/tests/coverage/coverage_tests.rs | 69 +- .../tests/coverage/py_api_coverage_tests.rs | 51 +- .../coverage/py_callable_coverage_tests.rs | 71 +- docs/about-nemo-relay/concepts/middleware.mdx | 26 + .../dynamic-plugins/native-dynamic/about.mdx | 39 +- docs/reference/event-sanitizers.mdx | 53 +- docs/reference/migration-guides.mdx | 114 ++- integrations/openclaw/test/live-smoke.test.ts | 14 +- python/nemo_relay/__init__.py | 23 +- python/nemo_relay/__init__.pyi | 25 +- python/nemo_relay/_native.pyi | 47 +- python/tests/test_adaptive.py | 2 +- python/tests/test_builtin_codecs.py | 9 +- python/tests/test_context_isolation.py | 8 +- python/tests/test_event_sanitizers.py | 4 +- python/tests/test_llm.py | 31 +- python/tests/test_tools.py | 8 +- 77 files changed, 6186 insertions(+), 2453 deletions(-) create mode 100644 crates/adaptive/tests/support/mod.rs create mode 100644 crates/core/tests/coverage/logging_rotation_tests.rs create mode 100644 crates/core/tests/integration/test_support.rs diff --git a/crates/adaptive/src/acg_component.rs b/crates/adaptive/src/acg_component.rs index 443e376ff..c71ec811d 100644 --- a/crates/adaptive/src/acg_component.rs +++ b/crates/adaptive/src/acg_component.rs @@ -582,26 +582,32 @@ pub(crate) fn create_acg_llm_request_intercept( provider: String, plugin: Arc, ) -> LlmRequestInterceptFn { - Arc::new(move |_name: &str, request: LlmRequest, annotated| { - let input_content = request.content.clone(); - let translated = - translate_request(&request, &agent_id, &provider, plugin.as_ref(), &hot_cache) - .unwrap_or(request); - if annotated.is_some() && translated.content != input_content { - let translated_annotated = build_semantic_request_view(&translated) - .map_err(|error| nemo_relay::error::FlowError::Internal(error.to_string()))? - .annotated_request; - return Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - LlmRequest { - headers: translated.headers, - content: input_content, - }, - Some(translated_annotated), - )); - } - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - translated, annotated, - )) + Arc::new(move |_name: String, request: LlmRequest, annotated| { + let hot_cache = hot_cache.clone(); + let agent_id = agent_id.clone(); + let provider = provider.clone(); + let plugin = plugin.clone(); + Box::pin(async move { + let input_content = request.content.clone(); + let translated = + translate_request(&request, &agent_id, &provider, plugin.as_ref(), &hot_cache) + .unwrap_or(request); + if annotated.is_some() && translated.content != input_content { + let translated_annotated = build_semantic_request_view(&translated) + .map_err(|error| nemo_relay::error::FlowError::Internal(error.to_string()))? + .annotated_request; + return Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + LlmRequest { + headers: translated.headers, + content: input_content, + }, + Some(translated_annotated), + )); + } + Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + translated, annotated, + )) + }) }) } diff --git a/crates/adaptive/src/adaptive_hints_intercept.rs b/crates/adaptive/src/adaptive_hints_intercept.rs index c9f245505..b4b649264 100644 --- a/crates/adaptive/src/adaptive_hints_intercept.rs +++ b/crates/adaptive/src/adaptive_hints_intercept.rs @@ -174,31 +174,34 @@ impl AdaptiveHintsIntercept { pub fn into_request_fn(self) -> LlmRequestInterceptFn { let this = Arc::new(self); Arc::new( - move |_name: &str, + move |_name: String, mut request: LlmRequest, mut annotated: Option| { - let scope_path = extract_scope_path(); - let manual_ls = read_manual_latency_sensitivity(); - let scope_depth = scope_path.len(); - let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed); - - let effective_agent_id = this.effective_agent_id(); - let cached_hints = - this.load_hints(&scope_path, &effective_agent_id, call_index, scope_depth); - let final_hints = apply_manual_latency_override( - cached_hints, - manual_ls, - &effective_agent_id, - scope_depth, - ); - - if let Some(hints) = final_hints { - inject_agent_hints(&mut request, &mut annotated, &hints); - } - - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - request, annotated, - )) + let this = this.clone(); + Box::pin(async move { + let scope_path = extract_scope_path(); + let manual_ls = read_manual_latency_sensitivity(); + let scope_depth = scope_path.len(); + let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed); + + let effective_agent_id = this.effective_agent_id(); + let cached_hints = + this.load_hints(&scope_path, &effective_agent_id, call_index, scope_depth); + let final_hints = apply_manual_latency_override( + cached_hints, + manual_ls, + &effective_agent_id, + scope_depth, + ); + + if let Some(hints) = final_hints { + inject_agent_hints(&mut request, &mut annotated, &hints); + } + + Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + request, annotated, + )) + }) }, ) } diff --git a/crates/adaptive/src/lib.rs b/crates/adaptive/src/lib.rs index ebe78d534..74ec930b7 100644 --- a/crates/adaptive/src/lib.rs +++ b/crates/adaptive/src/lib.rs @@ -14,6 +14,10 @@ pub(crate) static TEST_GLOBAL_CONTEXT_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); +#[cfg(test)] +#[path = "../tests/support/mod.rs"] +pub(crate) mod test_support; + pub mod acg; pub mod acg_component; pub mod acg_learner; diff --git a/crates/adaptive/tests/integration/runtime_integration_tests.rs b/crates/adaptive/tests/integration/runtime_integration_tests.rs index dd65079ea..f0e64fb1a 100644 --- a/crates/adaptive/tests/integration/runtime_integration_tests.rs +++ b/crates/adaptive/tests/integration/runtime_integration_tests.rs @@ -605,6 +605,7 @@ async fn test_adaptive_plugin_registers_and_passes_calls_through() { content: json!({"messages": []}), }, ) + .await .unwrap(); assert_eq!(request.request.content["messages"], json!([])); @@ -739,9 +740,11 @@ impl Plugin for HeaderPlugin { false, Arc::new(|_name, mut request, annotated| { request.headers.insert("x-plugin".into(), json!("set")); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - request, annotated, - )) + Box::pin(async move { + Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + request, annotated, + )) + }) }), )?; ctx.register_tool_request_intercept( @@ -752,7 +755,7 @@ impl Plugin for HeaderPlugin { if let Json::Object(ref mut map) = args { map.insert("x-tool-plugin".into(), json!(true)); } - Ok(args) + Box::pin(async move { Ok(args) }) }), )?; ctx.register_llm_execution_intercept( @@ -823,6 +826,7 @@ async fn test_top_level_plugin_registers_request_and_execution_intercepts() { content: json!({"messages": []}), }, ) + .await .unwrap(); assert_eq!(request.request.headers.get("x-plugin"), Some(&json!("set"))); diff --git a/crates/adaptive/tests/support/mod.rs b/crates/adaptive/tests/support/mod.rs new file mode 100644 index 000000000..c9f4775d0 --- /dev/null +++ b/crates/adaptive/tests/support/mod.rs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::future::Future; + +pub(crate) fn block_on(future: F) -> F::Output { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("test runtime should build") + .block_on(future) +} diff --git a/crates/adaptive/tests/unit/acg_component_tests.rs b/crates/adaptive/tests/unit/acg_component_tests.rs index 51477bb3b..69004ee72 100644 --- a/crates/adaptive/tests/unit/acg_component_tests.rs +++ b/crates/adaptive/tests/unit/acg_component_tests.rs @@ -1087,11 +1087,11 @@ fn acg_component_request_intercept_passes_original_request_and_annotation_when_t plugin, ); - let outcome = intercept( - "anthropic", + let outcome = crate::test_support::block_on(intercept( + "anthropic".to_string(), invalid_request.clone(), Some(annotated.clone()), - ) + )) .expect("request intercept should pass through"); let translated = outcome.request; let returned_annotated = outcome.annotated_request; @@ -1335,7 +1335,12 @@ fn acg_component_request_intercept_rewrites_annotation_without_mutating_provider plugin, ); - let outcome = intercept("anthropic", request, Some(original_annotation.clone())).unwrap(); + let outcome = crate::test_support::block_on(intercept( + "anthropic".to_string(), + request, + Some(original_annotation.clone()), + )) + .unwrap(); assert_eq!(outcome.request.content, original_content); let annotation = outcome diff --git a/crates/adaptive/tests/unit/adaptive_hints_intercept_tests.rs b/crates/adaptive/tests/unit/adaptive_hints_intercept_tests.rs index e3ef485b5..19296c842 100644 --- a/crates/adaptive/tests/unit/adaptive_hints_intercept_tests.rs +++ b/crates/adaptive/tests/unit/adaptive_hints_intercept_tests.rs @@ -4,6 +4,7 @@ //! Unit tests for adaptive hints intercept in the NeMo Relay adaptive crate. use super::*; + use std::sync::{Mutex, OnceLock}; use crate::trie::data_models::{LlmCallPrediction, PredictionMetrics}; @@ -196,14 +197,14 @@ fn test_adaptive_hints_intercept_injects_prediction_hints_and_manual_override() stream: None, extra: serde_json::Map::new(), }; - let outcome = req_fn( - "model", + let outcome = crate::test_support::block_on(req_fn( + "model".to_string(), LlmRequest { headers: serde_json::Map::new(), content: serde_json::json!({}), }, Some(annotated.clone()), - ) + )) .unwrap(); let request = outcome.request; let returned_annotated = outcome.annotated_request; @@ -266,14 +267,14 @@ fn test_adaptive_hints_intercept_uses_defaults_and_ignores_poisoned_cache() { })); let req_fn = AdaptiveHintsIntercept::new(hot_cache, "fallback-agent".to_string()).into_request_fn(); - let outcome = req_fn( - "model", + let outcome = crate::test_support::block_on(req_fn( + "model".to_string(), LlmRequest { headers: serde_json::Map::new(), content: serde_json::json!({}), }, None, - ) + )) .unwrap(); let request = outcome.request; let annotated = outcome.annotated_request; @@ -305,14 +306,14 @@ fn test_adaptive_hints_intercept_uses_defaults_and_ignores_poisoned_cache() { }); let poisoned_req_fn = AdaptiveHintsIntercept::new(poisoned_cache, "fallback-agent".to_string()).into_request_fn(); - let poisoned_outcome = poisoned_req_fn( - "model", + let poisoned_outcome = crate::test_support::block_on(poisoned_req_fn( + "model".to_string(), LlmRequest { headers: serde_json::Map::new(), content: serde_json::json!({"existing": true}), }, None, - ) + )) .unwrap(); let poisoned_request = poisoned_outcome.request; assert!( diff --git a/crates/adaptive/tests/unit/plugin_component_tests.rs b/crates/adaptive/tests/unit/plugin_component_tests.rs index 044504d19..5a49feb25 100644 --- a/crates/adaptive/tests/unit/plugin_component_tests.rs +++ b/crates/adaptive/tests/unit/plugin_component_tests.rs @@ -364,6 +364,7 @@ async fn adaptive_plugin_registers_runtime_and_rolls_back_registration() { content: json!({}), }, ) + .await .unwrap(); assert!(request.request.headers.is_empty()); diff --git a/crates/adaptive/tests/unit/runtime_features_tests.rs b/crates/adaptive/tests/unit/runtime_features_tests.rs index 5a53c7734..efdb60541 100644 --- a/crates/adaptive/tests/unit/runtime_features_tests.rs +++ b/crates/adaptive/tests/unit/runtime_features_tests.rs @@ -139,9 +139,11 @@ fn assert_llm_request_intercept_registered(name: &str) { i32::MAX, false, Arc::new(|_name, request, annotated| { - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - request, annotated, - )) + Box::pin(async move { + Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + request, annotated, + )) + }) }), ), name, @@ -154,9 +156,11 @@ fn assert_llm_request_intercept_absent(name: &str) { i32::MAX, false, Arc::new(|_name, request, annotated| { - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - request, annotated, - )) + Box::pin(async move { + Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + request, annotated, + )) + }) }), ) .unwrap(); @@ -565,6 +569,7 @@ async fn adaptive_hints_feature_registers_request_intercept() { content: json!({}), }, ) + .await .unwrap(); assert!(request.request.headers.contains_key(AGENT_HINTS_HEADER_KEY)); @@ -730,9 +735,11 @@ async fn registration_context_registers_all_supported_callback_types() { 5, false, Arc::new(|_name, request, annotated| { - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - request, annotated, - )) + Box::pin(async move { + Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + request, annotated, + )) + }) }), ) .unwrap(); diff --git a/crates/adaptive/tests/unit/runtime_tests.rs b/crates/adaptive/tests/unit/runtime_tests.rs index f14d83fb0..cfad601bd 100644 --- a/crates/adaptive/tests/unit/runtime_tests.rs +++ b/crates/adaptive/tests/unit/runtime_tests.rs @@ -637,6 +637,7 @@ async fn adaptive_runtime_bind_scope_requires_registration_and_passes_through_wi }; let translated = llm_request_intercepts("anthropic", request.clone()) + .await .expect("request intercept chain should pass through when no hot-cache state exists"); assert_eq!(translated.request.content, request.content); diff --git a/crates/cli/src/sessions/mod.rs b/crates/cli/src/sessions/mod.rs index 732f9c31e..5f5510966 100644 --- a/crates/cli/src/sessions/mod.rs +++ b/crates/cli/src/sessions/mod.rs @@ -582,6 +582,10 @@ impl SessionManager { .map_err(CliError::from) }) .await?; + // Manual lifecycle events publish on the serial dispatcher. This + // test-only seam returns after the matching end event is observable so + // a subsequent synthetic provider call cannot overtake it. + nemo_relay::api::subscriber::flush_subscribers().map_err(CliError::from)?; let mut sessions = self.inner.lock().await; if let Some(session) = sessions.get_mut(&session_id) { session.record_completed_llm_response(response_for_hints, owner_subagent_id); @@ -784,9 +788,9 @@ impl Session { NormalizedEvent::SubagentStarted(event) => self.start_subagent(event).await, NormalizedEvent::SubagentEnded(event) => self.end_subagent(event).await, NormalizedEvent::LlmHint(event) => self.add_llm_hint(event), - NormalizedEvent::LlmStarted(event) => self.start_hook_llm(event), - NormalizedEvent::LlmEnded(event) => self.end_hook_llm(event), - NormalizedEvent::ToolStarted(event) => self.start_tool(event), + NormalizedEvent::LlmStarted(event) => self.start_hook_llm(event).await, + NormalizedEvent::LlmEnded(event) => self.end_hook_llm(event).await, + NormalizedEvent::ToolStarted(event) => self.start_tool(event).await, NormalizedEvent::ToolEnded(event) => self.end_tool(event).await, NormalizedEvent::PromptSubmitted(event) => self.start_turn(event).await, NormalizedEvent::Compaction(event) => self.mark("compaction", event), @@ -1142,8 +1146,8 @@ impl Session { if self.turn_scope.is_none() { return Ok(Vec::new()); } - self.close_active_llms(reason)?; - self.close_active_tools(reason)?; + self.close_active_llms(reason).await?; + self.close_active_tools(reason).await?; let closed_subagents = self.close_active_subagents(reason).await?; let output = self.last_turn_llm_output.take().unwrap_or(output); self.clear_correlation_state(); @@ -1182,7 +1186,7 @@ impl Session { } // Ends all active hook-observed LLM calls before closing their containing scopes. - fn close_active_llms(&mut self, reason: &str) -> Result<(), CliError> { + async fn close_active_llms(&mut self, reason: &str) -> Result<(), CliError> { let active_llms: Vec<_> = self.llms.drain().map(|(_, handle)| handle).collect(); for handle in active_llms { llm_call_end( @@ -1198,7 +1202,7 @@ impl Session { // Ends all active tool calls with a synthetic close result before ending their containing scopes. // Draining first avoids holding mutable map state while the runtime emits lifecycle events. - fn close_active_tools(&mut self, reason: &str) -> Result<(), CliError> { + async fn close_active_tools(&mut self, reason: &str) -> Result<(), CliError> { let active_tools: Vec<_> = self .tools .drain() @@ -1428,7 +1432,7 @@ impl Session { // ignored so repeated pre hooks do not create parallel handles for one provider call. Aliased // child-session LLMs carry their subagent owner in metadata and are resolved by // `hook_llm_owner`. - fn start_hook_llm(&mut self, event: LlmEvent) -> Result<(), CliError> { + async fn start_hook_llm(&mut self, event: LlmEvent) -> Result<(), CliError> { self.ensure_turn_started(event.metadata.clone())?; if self.llms.contains_key(&event.api_call_id) { return Ok(()); @@ -1454,7 +1458,7 @@ impl Session { // Ends a hook-observed LLM call, synthesizing a start if only the post hook arrives. The same // alias metadata recovery used by `start_hook_llm` keeps post-only aliased child LLMs under the // subagent instead of falling back to the root agent. - fn end_hook_llm(&mut self, event: LlmEvent) -> Result<(), CliError> { + async fn end_hook_llm(&mut self, event: LlmEvent) -> Result<(), CliError> { self.ensure_turn_started(event.metadata.clone())?; let (parent, metadata) = self.hook_llm_owner(event.metadata); let handle = match self.llms.remove(&event.api_call_id) { @@ -1511,7 +1515,7 @@ impl Session { // Starts a tool call under an explicit subagent when available, otherwise under the turn // scope. Duplicate tool IDs are ignored so repeated pre-tool hooks do not create parallel // handles for one agent tool invocation. - fn start_tool(&mut self, event: ToolEvent) -> Result<(), CliError> { + async fn start_tool(&mut self, event: ToolEvent) -> Result<(), CliError> { self.ensure_turn_started(event.metadata.clone())?; if self.tools.contains_key(&event.tool_call_id) { return Ok(()); @@ -1529,7 +1533,7 @@ impl Session { let active_tool_arguments = arguments.clone(); let active_tool_name = event.tool_name.clone(); let active_tool_owner_subagent_id = owner.subagent_id.clone(); - tool_conditional_execution(event.tool_name.as_str(), &arguments)?; + tool_conditional_execution(event.tool_name.as_str(), &arguments).await?; let metadata = tool_correlation_metadata( self.event_identity_metadata(event.metadata), owner.status, diff --git a/crates/cli/tests/coverage/shared/server_tests.rs b/crates/cli/tests/coverage/shared/server_tests.rs index da7de6ea6..558415e2e 100644 --- a/crates/cli/tests/coverage/shared/server_tests.rs +++ b/crates/cli/tests/coverage/shared/server_tests.rs @@ -2339,7 +2339,9 @@ async fn pre_tool_hook_rejects_when_conditional_guardrail_blocks() { "cli-pre-tool-blocker", 1, Arc::new(|name, _args| { - Ok((name == BLOCKED_TEST_TOOL).then(|| "blocked by policy".to_string())) + Box::pin(async move { + Ok((name == BLOCKED_TEST_TOOL).then(|| "blocked by policy".to_string())) + }) }), ) .unwrap(); @@ -2565,21 +2567,24 @@ async fn gateway_request_codec_exposes_annotations_and_applies_buffered_edits() 1, false, Arc::new(move |_name, mut request, annotated| { - if request.headers.get("x-codec-test").and_then(Value::as_str) != Some("buffered") { - return Ok(LlmRequestInterceptOutcome::new(request, annotated)); - } - let mut annotated = annotated.expect("gateway generation route must have a codec"); - *captured.lock().unwrap() = Some(serde_json::to_value(&annotated).unwrap()); - let nemo_relay::codec::request::Message::User { content, .. } = - &mut annotated.messages[0] - else { - panic!("expected portable Responses string input"); - }; - *content = nemo_relay::codec::request::MessageContent::Text("edited".into()); - request - .headers - .insert("x-test-intercept".into(), json!("visible")); - Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + let captured = captured.clone(); + Box::pin(async move { + if request.headers.get("x-codec-test").and_then(Value::as_str) != Some("buffered") { + return Ok(LlmRequestInterceptOutcome::new(request, annotated)); + } + let mut annotated = annotated.expect("gateway generation route must have a codec"); + *captured.lock().unwrap() = Some(serde_json::to_value(&annotated).unwrap()); + let nemo_relay::codec::request::Message::User { content, .. } = + &mut annotated.messages[0] + else { + panic!("expected portable Responses string input"); + }; + *content = nemo_relay::codec::request::MessageContent::Text("edited".into()); + request + .headers + .insert("x-test-intercept".into(), json!("visible")); + Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + }) }), ) .unwrap(); @@ -2622,10 +2627,12 @@ async fn gateway_request_codec_rejects_raw_body_mutation_before_upstream() { 1, false, Arc::new(|_name, mut request, annotated| { - if request.headers.get("x-codec-test").and_then(Value::as_str) == Some("raw") { - request.content["input"] = json!("forbidden raw edit"); - } - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + Box::pin(async move { + if request.headers.get("x-codec-test").and_then(Value::as_str) == Some("raw") { + request.content["input"] = json!("forbidden raw edit"); + } + Ok(LlmRequestInterceptOutcome::new(request, annotated)) + }) }), ) .unwrap(); @@ -2688,17 +2695,20 @@ async fn gateway_request_codec_rejects_stream_mode_changes_before_upstream() { 1, false, Arc::new(|_name, request, annotated| { - if request - .headers - .get("x-codec-stream-toggle") - .and_then(Value::as_str) - == Some("true") - { - let mut annotated = annotated.expect("generation route must expose an annotation"); - annotated.stream = Some(!annotated.stream.unwrap_or(false)); - return Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))); - } - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + Box::pin(async move { + if request + .headers + .get("x-codec-stream-toggle") + .and_then(Value::as_str) + == Some("true") + { + let mut annotated = + annotated.expect("generation route must expose an annotation"); + annotated.stream = Some(!annotated.stream.unwrap_or(false)); + return Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))); + } + Ok(LlmRequestInterceptOutcome::new(request, annotated)) + }) }), ) .unwrap(); @@ -2746,29 +2756,33 @@ async fn gateway_request_codecs_apply_buffered_and_streaming_edits_on_all_genera 1, false, Arc::new(move |_name, mut request, annotated| { - let Some(marker) = request - .headers - .get("x-codec-matrix") - .and_then(Value::as_str) - .map(str::to_string) - else { - return Ok(LlmRequestInterceptOutcome::new(request, annotated)); - }; - let mut annotated = annotated.expect("generation route must expose an annotation"); - captured_annotations.lock().unwrap().push(json!({ - "marker": marker, - "annotation": annotated, - })); - let nemo_relay::codec::request::Message::User { content, .. } = - &mut annotated.messages[0] - else { - panic!("expected the first request item to be a portable user message"); - }; - *content = nemo_relay::codec::request::MessageContent::Text(format!("edited-{marker}")); - request - .headers - .insert("x-codec-edited".into(), json!(marker)); - Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + let captured_annotations = captured_annotations.clone(); + Box::pin(async move { + let Some(marker) = request + .headers + .get("x-codec-matrix") + .and_then(Value::as_str) + .map(str::to_string) + else { + return Ok(LlmRequestInterceptOutcome::new(request, annotated)); + }; + let mut annotated = annotated.expect("generation route must expose an annotation"); + captured_annotations.lock().unwrap().push(json!({ + "marker": marker, + "annotation": annotated, + })); + let nemo_relay::codec::request::Message::User { content, .. } = + &mut annotated.messages[0] + else { + panic!("expected the first request item to be a portable user message"); + }; + *content = + nemo_relay::codec::request::MessageContent::Text(format!("edited-{marker}")); + request + .headers + .insert("x-codec-edited".into(), json!(marker)); + Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + }) }), ) .unwrap(); diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index c34829241..06ac5119b 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +use std::future::Future; use std::sync::Arc; use chrono::{DateTime, TimeDelta, Utc}; @@ -19,6 +20,9 @@ use crate::api::optimization::{ use crate::api::runtime::LlmCodecIdentity; use crate::api::runtime::NemoRelayContextState; use crate::api::runtime::global_context; +use crate::api::runtime::subscriber_dispatcher::{ + dispatch_sanitized_event, dispatch_transformed_event, +}; use crate::api::runtime::{ EventSubscriberFn, LlmCollectorFn, LlmExecutionNextFn, LlmFinalizerFn, LlmJsonStream, LlmSanitizeRequestContext, LlmSanitizeResponseContext, LlmStreamExecutionNextFn, @@ -30,7 +34,7 @@ use crate::api::scope::{EmitMarkEventParams, ScopeHandle}; use crate::api::shared::{ ensure_runtime_owner, inject_dynamo_session_ids, metadata_with_otel_status, resolve_parent_uuid, run_request_intercepts_with_codec_and_recorder, - sanitize_event_with_scope_stack, snapshot_event_subscribers, + sanitize_event_with_scope_stack, snapshot_event_sanitizers, snapshot_event_subscribers, }; use crate::codec::request::{AnnotatedLlmRequest, Message}; use crate::codec::response::{AnnotatedLlmResponse, attach_estimated_cost_for_provider}; @@ -400,28 +404,7 @@ fn limit_annotated_request_history_to_current_user_turn( ) } -fn emit_llm_start( - handle: &LlmHandle, - request: &LlmRequest, - annotated_request: Option>, - request_codec: Option>, -) -> Result<()> { - ensure_runtime_owner()?; - let subscribers = { - let scope_stack = handle.captured_scope_stack(); - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); - snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())? - }; - emit_llm_start_with_subscribers( - handle, - request, - annotated_request, - request_codec, - &subscribers, - ) -} - -fn emit_llm_start_with_subscribers( +async fn emit_llm_start_with_subscribers( handle: &LlmHandle, request: &LlmRequest, annotated_request: Option>, @@ -446,7 +429,8 @@ fn emit_llm_start_with_subscribers( observable_request.clone(), LlmSanitizeRequestContext::for_request_codec(request_codec.clone()), &entries, - ); + ) + .await; let request_changed = sanitized_request .as_ref() .is_some_and(|sanitized_request| sanitized_request != &observable_request); @@ -480,7 +464,7 @@ fn emit_llm_start_with_subscribers( .map_err(|error| FlowError::Internal(error.to_string()))?; state.build_llm_start_event(handle, input, annotated_request) }; - if let Some(event) = sanitize_event_with_scope_stack(event, scope_stack) { + if let Some(event) = sanitize_event_with_scope_stack(event, scope_stack).await { NemoRelayContextState::emit_event(&event, subscribers); } Ok(()) @@ -495,7 +479,33 @@ fn remove_observability_credential_headers(mut request: LlmRequest) -> LlmReques request } -fn emit_pending_request_marks( +/// Synchronous test seam retained for lifecycle unit tests. Public manual +/// lifecycle emission is synchronous too, but its work is queued; this helper +/// exercises the managed start-event transformation directly. +#[cfg(test)] +fn emit_llm_start( + handle: &LlmHandle, + request: &LlmRequest, + annotated_request: Option>, + request_codec: Option>, +) -> Result<()> { + let subscribers = { + let scope_stack = handle.captured_scope_stack(); + let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())? + }; + tokio::runtime::Runtime::new() + .map_err(|error| FlowError::Internal(error.to_string()))? + .block_on(emit_llm_start_with_subscribers( + handle, + request, + annotated_request, + request_codec, + &subscribers, + )) +} + +async fn emit_pending_request_marks( handle: &LlmHandle, marks: Vec, subscribers: &[EventSubscriberFn], @@ -517,28 +527,58 @@ fn emit_pending_request_marks( mark.category, mark.category_profile, )); - if let Some(event) = sanitize_event_with_scope_stack(event, handle.captured_scope_stack()) { + if let Some(event) = + sanitize_event_with_scope_stack(event, handle.captured_scope_stack()).await + { NemoRelayContextState::emit_event(&event, subscribers); } } Ok(()) } -pub(crate) fn emit_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriberFn]) { - emit_optimization_marks_with( +pub(crate) async fn emit_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriberFn]) { + emit_optimization_marks_with_async( handle, subscribers, |event| sanitize_event_with_scope_stack(event, handle.captured_scope_stack()), |event, subscribers| NemoRelayContextState::try_emit_event(event, subscribers), - ); + ) + .await; } -fn emit_optimization_marks_with( +/// Queue optimization marks from a synchronous lifecycle API. +/// +/// The public manual lifecycle APIs must not await middleware. Capture each +/// event's sanitizer chain now and enqueue the immutable snapshots ahead of +/// the corresponding end event, preserving publication order. +fn enqueue_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriberFn]) { + let contributions = handle.optimization_recorder.unemitted_with_timestamps(); + if contributions.is_empty() || ensure_runtime_owner().is_err() { + return; + } + let scope_stack = handle.captured_scope_stack().clone(); + for (contribution, recorded_at) in contributions { + let event = optimization_mark_event(handle, &contribution, recorded_at); + let Some(sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) else { + break; + }; + if dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack.clone()) { + handle.optimization_recorder.mark_emitted(1); + } else { + break; + } + } +} + +async fn emit_optimization_marks_with_async( handle: &LlmHandle, subscribers: &[EventSubscriberFn], - mut sanitize: impl FnMut(Event) -> Option, + mut sanitize: F, mut enqueue: impl FnMut(&Event, &[EventSubscriberFn]) -> bool, -) { +) where + F: FnMut(Event) -> Fut, + Fut: Future>, +{ let contributions = handle.optimization_recorder.unemitted_with_timestamps(); if contributions.is_empty() { return; @@ -554,30 +594,8 @@ fn emit_optimization_marks_with( return; } for (contribution, recorded_at) in contributions { - let offset = contribution.sequence.unwrap_or(0).saturating_add(2); - let offset = i64::try_from(offset).unwrap_or(i64::MAX); - let request_ordered_timestamp = handle.started_at + TimeDelta::microseconds(offset); - let timestamp = recorded_at.max(request_ordered_timestamp); - let data = serde_json::to_value(&contribution).unwrap_or(Json::Null); - let event = Event::Mark(MarkEvent::new( - BaseEvent::builder() - .name("nemo_relay.llm.optimization") - .parent_uuid(handle.uuid) - .timestamp(timestamp) - .data(data) - .data_schema(DataSchema { - name: "nemo.relay.llm_optimization_contribution".to_string(), - version: "1".to_string(), - }) - .build(), - Some(EventCategory::custom()), - Some( - CategoryProfile::builder() - .subtype("nemo_relay.llm.optimization") - .build(), - ), - )); - let Some(event) = sanitize(event) else { + let event = optimization_mark_event(handle, &contribution, recorded_at); + let Some(event) = sanitize(event).await else { // Sanitizers currently rewrite fields rather than intentionally // dropping events. `None` means the sanitizer context was // unavailable, so preserve this ordered suffix for a later retry. @@ -594,6 +612,63 @@ fn emit_optimization_marks_with( } } +fn optimization_mark_event( + handle: &LlmHandle, + contribution: &crate::codec::optimization::LlmOptimizationContribution, + recorded_at: DateTime, +) -> Event { + let offset = contribution.sequence.unwrap_or(0).saturating_add(2); + let offset = i64::try_from(offset).unwrap_or(i64::MAX); + let request_ordered_timestamp = handle.started_at + TimeDelta::microseconds(offset); + Event::Mark(MarkEvent::new( + BaseEvent::builder() + .name("nemo_relay.llm.optimization") + .parent_uuid(handle.uuid) + .timestamp(recorded_at.max(request_ordered_timestamp)) + .data(serde_json::to_value(contribution).unwrap_or(Json::Null)) + .data_schema(DataSchema { + name: "nemo.relay.llm_optimization_contribution".to_string(), + version: "1".to_string(), + }) + .build(), + Some(EventCategory::custom()), + Some( + CategoryProfile::builder() + .subtype("nemo_relay.llm.optimization") + .build(), + ), + )) +} + +/// Synchronous test seam for optimization-mark accounting. Production paths +/// always use [`emit_optimization_marks_with_async`]; unit tests use this seam +/// to isolate cursor behavior from asynchronous event publication. +#[cfg(test)] +fn emit_optimization_marks_with( + handle: &LlmHandle, + subscribers: &[EventSubscriberFn], + mut sanitize: F, + mut enqueue: impl FnMut(&Event, &[EventSubscriberFn]) -> bool, +) where + F: FnMut(Event) -> Option, +{ + let contributions = handle.optimization_recorder.unemitted_with_timestamps(); + if contributions.is_empty() || ensure_runtime_owner().is_err() { + return; + } + for (contribution, recorded_at) in contributions { + let event = optimization_mark_event(handle, &contribution, recorded_at); + let Some(event) = sanitize(event) else { + break; + }; + if enqueue(&event, subscribers) { + handle.optimization_recorder.mark_emitted(1); + } else { + break; + } + } +} + /// Start a manual LLM lifecycle span. /// /// This emits an LLM-start event after applying sanitize-request guardrails to @@ -641,7 +716,77 @@ pub fn llm_call(params: LlmCallParams<'_>) -> Result { .timestamp_opt(params.timestamp) .build(); let handle = create_llm_handle(handle_params)?; - emit_llm_start(&handle, params.request, params.annotated_request, None)?; + let scope_stack = handle.captured_scope_stack().clone(); + let (entries, subscribers, agent_is_fresh) = { + let mut scope_guard = scope_stack.write().expect("scope stack lock poisoned"); + let scope_locals = scope_guard.collect_scope_local_registries(|registries| { + ®istries.llm_sanitize_request_guardrails + }); + let subscribers = + snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?; + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + let entries = state.llm_sanitize_request_entries(&scope_locals); + drop(state); + let agent_is_fresh = scope_guard.take_agent_freshness(handle.parent_uuid); + (entries, subscribers, agent_is_fresh) + }; + // Middleware and event publication only observe a credential-free copy. + // Keep `params.request` untouched: it remains the caller/provider request. + let request = remove_observability_credential_headers(params.request.clone()); + let annotated_request = params.annotated_request; + let event = { + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + state.build_llm_start_event(&handle, None, None) + }; + let queued_handle = handle.clone(); + if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { + dispatch_transformed_event( + event, + Box::new(move |event| { + Box::pin(async move { + let mut sanitized_request = + NemoRelayContextState::llm_sanitize_request_snapshot_chain( + request.clone(), + LlmSanitizeRequestContext::default(), + &entries, + ) + .await; + let request_changed = sanitized_request + .as_ref() + .is_some_and(|sanitized| sanitized != &request); + let mut annotation = if sanitized_request.is_none() || request_changed { + None + } else { + annotated_request + }; + if !agent_is_fresh && let Some(sanitized_request) = sanitized_request.as_mut() { + project_llm_request_to_current_user_turn( + sanitized_request, + &mut annotation, + None, + ); + } + let input = sanitized_request + .as_ref() + .and_then(|request| serde_json::to_value(request).ok()); + let context = global_context(); + match context.read() { + Ok(state) => state.build_llm_start_event(&queued_handle, input, annotation), + Err(_) => event, + } + }) + }), + event_sanitizers, + &subscribers, + scope_stack, + ); + } Ok(handle) } @@ -682,17 +827,137 @@ struct LlmCallEndBehavior { /// Sanitize-response guardrails affect only the emitted end-event payload, not /// the caller-owned `response` value. pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { - llm_call_end_with_behavior( - params, - LlmCallEndBehavior { - response_codec_errors_fatal: true, - attach_estimated_cost: false, - }, - None, - ) + ensure_runtime_owner()?; + let scope_stack = params.handle.captured_scope_stack().clone(); + let (entries, subscribers) = { + let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_locals = scope_guard.collect_scope_local_registries(|registries| { + ®istries.llm_sanitize_response_guardrails + }); + let subscribers = + snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?; + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + ( + state.llm_sanitize_response_entries(&scope_locals), + subscribers, + ) + }; + let response = if params.response.is_null() { + params.data.unwrap_or(params.response) + } else { + params.response + }; + let response_was_null_without_fallback = response.is_null(); + let handle = params.handle.clone(); + let metadata = params.metadata; + let timestamp = params.timestamp; + let annotated_response = params.annotated_response; + let response_codec = params.response_codec; + handle.optimization_recorder.close_for_finalization(None); + enqueue_optimization_marks(&handle, &subscribers); + let event = { + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + state.build_llm_end_event( + EndLlmHandleParams::builder() + .handle(&handle) + .data(Json::Null) + .metadata_opt(metadata.clone()) + .annotated_response_opt(annotated_response.clone()) + .timestamp_opt(timestamp) + .build(), + ) + }; + if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { + dispatch_transformed_event( + event, + Box::new(move |event| { + Box::pin(async move { + let sanitized = NemoRelayContextState::llm_sanitize_response_snapshot_chain( + response.clone(), + LlmSanitizeResponseContext::for_response_codec(response_codec.clone()), + &entries, + ) + .await; + let changed = sanitized + .as_ref() + .is_some_and(|sanitized| sanitized != &response); + let data = match sanitized { + Some(response) + if response_was_null_without_fallback && response.is_null() => + { + None + } + response => response, + }; + let annotation_omitted = data.as_ref().is_none_or(Json::is_null); + let (mut annotation, decode_error) = if annotation_omitted { + (None, None) + } else { + resolve_llm_end_annotation( + (!changed).then_some(annotated_response).flatten(), + response_codec, + data.as_ref(), + &LlmCallEndBehavior { + response_codec_errors_fatal: false, + attach_estimated_cost: false, + }, + &handle.name, + ) + }; + if let Some(error) = decode_error { + log::error!( + target: "nemo_relay.runtime", + event = "manual_llm_response_codec_failed"; + "Manual LLM response annotation failed during queued publication: {error}" + ); + } + let pricing = crate::codec::response::active_pricing_resolver(); + let summary = finalize_optimization_summary( + &handle.optimization_recorder, + annotation.as_mut(), + handle.model_name.as_deref(), + &pricing, + ); + if !annotation_omitted + && annotation.is_none() + && let Some(summary) = summary + { + annotation = Some(AnnotatedLlmResponse { + optimization_summary: Some(summary), + ..AnnotatedLlmResponse::default() + }); + } + let context = global_context(); + let Ok(state) = context.read() else { + return event; + }; + let end_metadata = metadata_with_otel_status(metadata, "OK", None); + state.build_llm_end_event( + EndLlmHandleParams::builder() + .handle(&handle) + .data_opt(data) + .metadata_opt(end_metadata) + .annotated_response_opt(annotation.map(Arc::new)) + .timestamp_opt(timestamp) + .build(), + ) + }) + }), + event_sanitizers, + &subscribers, + scope_stack, + ); + } + Ok(()) } -fn llm_call_end_with_behavior( +async fn llm_call_end_with_behavior( params: LlmCallEndParams<'_>, behavior: LlmCallEndBehavior, lifecycle_subscribers: Option<&[EventSubscriberFn]>, @@ -735,7 +1000,8 @@ fn llm_call_end_with_behavior( response.clone(), LlmSanitizeResponseContext::for_response_codec(response_codec.clone()), &entries, - ); + ) + .await; let response_changed = sanitized_response .as_ref() .is_some_and(|sanitized_response| sanitized_response != &response); @@ -756,7 +1022,7 @@ fn llm_call_end_with_behavior( ) }; handle.optimization_recorder.close_for_finalization(None); - emit_optimization_marks(handle, &subscribers); + emit_optimization_marks(handle, &subscribers).await; let pricing = crate::codec::response::active_pricing_resolver(); let summary = finalize_optimization_summary( &handle.optimization_recorder, @@ -790,7 +1056,8 @@ fn llm_call_end_with_behavior( .build(), ) }; - if let Some(event) = sanitize_event_with_scope_stack(event, handle.captured_scope_stack()) { + if let Some(event) = sanitize_event_with_scope_stack(event, handle.captured_scope_stack()).await + { NemoRelayContextState::emit_event(&event, &subscribers); } if let Some(error) = decode_error @@ -842,7 +1109,7 @@ fn resolve_llm_end_annotation( } } -fn emit_llm_end_without_output( +async fn emit_llm_end_without_output( handle: &LlmHandle, metadata: Option, response_codec: Option>, @@ -868,17 +1135,20 @@ fn emit_llm_end_without_output( (entries, subscribers) }; let had_fallback_data = handle.data.is_some(); - let data = handle.data.clone().and_then(|data| { + let data = if let Some(data) = handle.data.clone() { NemoRelayContextState::llm_sanitize_response_snapshot_chain( data, LlmSanitizeResponseContext::for_response_codec(response_codec), &entries, ) - }); + .await + } else { + None + }; let annotation_omitted = (had_fallback_data && data.is_none()) || data.as_ref().is_some_and(Json::is_null); handle.optimization_recorder.close_for_finalization(None); - emit_optimization_marks(handle, &subscribers); + emit_optimization_marks(handle, &subscribers).await; let pricing = crate::codec::response::active_pricing_resolver(); let annotated_response = (!annotation_omitted) .then(|| { @@ -903,7 +1173,8 @@ fn emit_llm_end_without_output( .map_err(|error| FlowError::Internal(error.to_string()))?; state.end_llm_handle(handle, data, metadata, annotated_response) }; - if let Some(event) = sanitize_event_with_scope_stack(event, handle.captured_scope_stack()) { + if let Some(event) = sanitize_event_with_scope_stack(event, handle.captured_scope_stack()).await + { NemoRelayContextState::emit_event(&event, &subscribers); } Ok(()) @@ -990,7 +1261,9 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { &subscribers, parent_uuid, guardrail_metadata, - )? { + ) + .await? + { let mut rejection_data = json!({}); if let Some(object) = rejection_data.as_object_mut() { object.insert("rejected".into(), json!(true)); @@ -1018,6 +1291,7 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { codec, &optimization_recorder, ) + .await }) .await?; @@ -1043,12 +1317,13 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { annotated_request.clone(), request_codec.clone(), &lifecycle_subscribers, - )?; - emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers)?; + ) + .await?; + emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers).await?; handle .optimization_recorder .record_all(optimization_contributions); - emit_optimization_marks(&handle, &lifecycle_subscribers); + emit_optimization_marks(&handle, &lifecycle_subscribers).await; let execution_name = name.clone(); let event_uuid = handle.uuid; @@ -1087,7 +1362,8 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { attach_estimated_cost: true, }, Some(&lifecycle_subscribers), - )?; + ) + .await?; Ok(response) } Err(error) => { @@ -1098,7 +1374,8 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { end_metadata, response_codec, Some(&lifecycle_subscribers), - ); + ) + .await; Err(error) } } @@ -1186,7 +1463,9 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu &subscribers, parent_uuid, guardrail_metadata, - )? { + ) + .await? + { let mut rejection_data = json!({}); if let Some(object) = rejection_data.as_object_mut() { object.insert("rejected".into(), json!(true)); @@ -1214,6 +1493,7 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu codec, &optimization_recorder, ) + .await }) .await?; @@ -1239,12 +1519,13 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu annotated_request, request_codec.clone(), &lifecycle_subscribers, - )?; - emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers)?; + ) + .await?; + emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers).await?; handle .optimization_recorder .record_all(optimization_contributions); - emit_optimization_marks(&handle, &lifecycle_subscribers); + emit_optimization_marks(&handle, &lifecycle_subscribers).await; let execution_name = name.clone(); let event_uuid = handle.uuid; @@ -1289,7 +1570,8 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu end_metadata, response_codec, Some(&lifecycle_subscribers), - ); + ) + .await; Err(error) } } @@ -1318,7 +1600,7 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu /// /// This helper does not emit the returned marks because it does not own an LLM /// lifecycle. Callers must attach them to the lifecycle they own. -pub fn llm_request_intercepts( +pub async fn llm_request_intercepts( name: &str, request: LlmRequest, ) -> Result { @@ -1336,7 +1618,8 @@ pub fn llm_request_intercepts( }; let mut outcome = NemoRelayContextState::llm_request_intercepts_snapshot_chain( name, request, None, &entries, false, - )?; + ) + .await?; inject_dynamo_session_ids(&mut outcome.request); Ok(outcome) } @@ -1361,7 +1644,7 @@ pub fn llm_request_intercepts( /// This helper is useful for preflight checks when the caller needs the /// rejection result without starting an LLM span. Guardrail scopes are still /// emitted for the conditional checks themselves. -pub fn llm_conditional_execution(request: &LlmRequest) -> Result<()> { +pub async fn llm_conditional_execution(request: &LlmRequest) -> Result<()> { ensure_runtime_owner()?; let (entries, subscribers, parent_uuid) = { let scope_stack = current_scope_stack(); @@ -1384,7 +1667,9 @@ pub fn llm_conditional_execution(request: &LlmRequest) -> Result<()> { &subscribers, parent_uuid, None, - )? { + ) + .await? + { return Err(FlowError::GuardrailRejected(error)); } Ok(()) diff --git a/crates/core/src/api/runtime/callbacks.rs b/crates/core/src/api/runtime/callbacks.rs index e40558da1..b07a82fde 100644 --- a/crates/core/src/api/runtime/callbacks.rs +++ b/crates/core/src/api/runtime/callbacks.rs @@ -27,8 +27,14 @@ use crate::json::Json; /// /// The callback receives the current event as immutable context and the fields /// it may replace. Later callbacks observe fields returned by earlier entries. -pub type EventSanitizeFn = - Arc EventSanitizeFields + Send + Sync>; +pub type EventSanitizeFn = Arc< + dyn Fn( + Event, + EventSanitizeFields, + ) -> Pin> + Send>> + + Send + + Sync, +>; /// Sanitize a tool request payload before the runtime records it. /// @@ -42,7 +48,8 @@ pub type EventSanitizeFn = /// /// # Returns /// Sanitized JSON payload for the emitted event. -pub type ToolSanitizeFn = Arc Json + Send + Sync>; +pub type ToolSanitizeFn = + Arc Pin> + Send>> + Send + Sync>; /// Decide whether a tool call is allowed to continue. /// /// The callback receives the tool name and the current argument payload. It can @@ -64,7 +71,11 @@ pub type ToolSanitizeFn = Arc Json + Send + Sync>; /// # Errors /// The callback can return any [`FlowError`](crate::error::FlowError) to abort /// guardrail evaluation. -pub type ToolConditionalFn = Arc Result> + Send + Sync>; +pub type ToolConditionalFn = Arc< + dyn Fn(String, Json) -> Pin>> + Send>> + + Send + + Sync, +>; /// Rewrite tool arguments before execution. /// /// Tool request intercepts run in priority order and can transform the JSON @@ -80,7 +91,8 @@ pub type ToolConditionalFn = Arc Result> + /// # Errors /// The callback can return any [`FlowError`](crate::error::FlowError) to abort /// the request-intercept chain. -pub type ToolInterceptFn = Arc Result + Send + Sync>; +pub type ToolInterceptFn = + Arc Pin> + Send>> + Send + Sync>; /// Continuation type invoked by tool execution intercepts. /// /// Execution intercepts receive this callable as their `next` continuation and @@ -308,8 +320,14 @@ impl LlmSanitizeResponseContext { /// /// The context is always supplied and distinguishes no codec, built-in codecs, /// runtime-registered codecs, and opaque active codecs. -pub type LlmSanitizeRequestFn = - Arc Option + Send + Sync>; +pub type LlmSanitizeRequestFn = Arc< + dyn Fn( + LlmRequest, + LlmSanitizeRequestContext, + ) -> Pin>> + Send>> + + Send + + Sync, +>; /// Sanitize an LLM response before the runtime records it. /// /// These callbacks rewrite the JSON response payload captured on LLM-end @@ -325,8 +343,14 @@ pub type LlmSanitizeRequestFn = /// /// The context is always supplied and distinguishes no codec, built-in codecs, /// runtime-registered codecs, and opaque active codecs. -pub type LlmSanitizeResponseFn = - Arc Option + Send + Sync>; +pub type LlmSanitizeResponseFn = Arc< + dyn Fn( + Json, + LlmSanitizeResponseContext, + ) -> Pin>> + Send>> + + Send + + Sync, +>; /// Decide whether an LLM call is allowed to continue. /// /// The callback receives the current [`LlmRequest`] and can allow execution, @@ -346,7 +370,11 @@ pub type LlmSanitizeResponseFn = /// # Errors /// The callback can return any [`FlowError`](crate::error::FlowError) to abort /// guardrail evaluation. -pub type LlmConditionalFn = Arc Result> + Send + Sync>; +pub type LlmConditionalFn = Arc< + dyn Fn(LlmRequest) -> Pin>> + Send>> + + Send + + Sync, +>; /// Rewrite or annotate an LLM request before execution. /// /// Request intercepts can transform the wire request, attach or replace a @@ -368,7 +396,11 @@ pub type LlmConditionalFn = Arc Result> + /// The callback can return any [`FlowError`](crate::error::FlowError) to abort /// the request-intercept chain. pub type LlmRequestInterceptFn = Arc< - dyn Fn(&str, LlmRequest, Option) -> Result + dyn Fn( + String, + LlmRequest, + Option, + ) -> Pin> + Send>> + Send + Sync, >; diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index fa45397a4..e2098b9a7 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -10,7 +10,6 @@ use std::any::Any; use std::collections::HashMap; -use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -565,7 +564,7 @@ impl NemoRelayContextState { )) } - fn emit_guardrail_scope_start( + async fn emit_guardrail_scope_start( name: &str, parent_uuid: Option, metadata: Option, @@ -592,13 +591,13 @@ impl NemoRelayContextState { EventCategory::from(handle.scope_type), None, )); - if let Some(event) = sanitize_event(event) { + if let Some(event) = sanitize_event(event).await { Self::emit_event(&event, subscribers); } handle } - fn emit_guardrail_scope_end( + async fn emit_guardrail_scope_end( handle: &ScopeHandle, output: Json, subscribers: &[EventSubscriberFn], @@ -617,7 +616,7 @@ impl NemoRelayContextState { EventCategory::from(handle.scope_type), None, )); - if let Some(event) = sanitize_event(event) { + if let Some(event) = sanitize_event(event).await { Self::emit_event(&event, subscribers); } } @@ -634,23 +633,21 @@ impl NemoRelayContextState { } /// Apply an event sanitizer snapshot to the mutable observability fields. - pub(crate) fn event_sanitize_snapshot_chain( + pub(crate) async fn event_sanitize_snapshot_chain( mut event: Event, entries: &[Guardrail], ) -> Event { for entry in entries { - if catch_unwind(AssertUnwindSafe(|| { - let fields = (entry.payload)(&event, event.sanitize_fields()); - event.apply_sanitize_fields(fields); - })) - .is_err() - { - log::error!( + let fields = event.sanitize_fields(); + match (entry.payload)(event.clone(), fields).await { + Ok(fields) => event.apply_sanitize_fields(fields), + Err(error) => log::error!( target: "nemo_relay.runtime", - event = "event_sanitizer_panicked", - guardrail = entry.name.as_str(); - "Event sanitizer panicked; publishing the latest valid event snapshot" - ); + event = "event_sanitizer_failed", + sanitizer = entry.name.as_str(), + event_name = event.name(); + "Event sanitizer failed; preserving the last valid event snapshot: {error}" + ), } } event @@ -684,14 +681,23 @@ impl NemoRelayContextState { /// /// # Returns /// The sanitized JSON payload after every provided guardrail has run. - pub(crate) fn tool_sanitize_request_snapshot_chain( + pub(crate) async fn tool_sanitize_request_snapshot_chain( name: &str, args: Json, entries: &[Guardrail], ) -> Json { let mut value = args; for entry in entries { - value = (entry.payload)(name, value); + match (entry.payload)(name.to_string(), value.clone()).await { + Ok(next) => value = next, + Err(error) => log::error!( + target: "nemo_relay.runtime", + event = "tool_request_sanitizer_failed", + sanitizer = entry.name.as_str(), + tool_name = name; + "Tool request sanitizer failed; preserving the last valid payload: {error}" + ), + } } value } @@ -724,14 +730,23 @@ impl NemoRelayContextState { /// /// # Returns /// The sanitized JSON payload after every provided guardrail has run. - pub(crate) fn tool_sanitize_response_snapshot_chain( + pub(crate) async fn tool_sanitize_response_snapshot_chain( name: &str, result: Json, entries: &[Guardrail], ) -> Json { let mut value = result; for entry in entries { - value = (entry.payload)(name, value); + match (entry.payload)(name.to_string(), value.clone()).await { + Ok(next) => value = next, + Err(error) => log::error!( + target: "nemo_relay.runtime", + event = "tool_response_sanitizer_failed", + sanitizer = entry.name.as_str(), + tool_name = name; + "Tool response sanitizer failed; preserving the last valid payload: {error}" + ), + } } value } @@ -781,7 +796,7 @@ impl NemoRelayContextState { /// # Errors /// Propagates any error returned by a guardrail callback after emitting the /// corresponding guardrail scope end event. - pub(crate) fn tool_conditional_execution_snapshot_chain( + pub(crate) async fn tool_conditional_execution_snapshot_chain( name: &str, args: &Json, entries: &[Guardrail], @@ -799,8 +814,9 @@ impl NemoRelayContextState { "target_name": name, }), subscribers, - ); - let result = (entry.payload)(name, args); + ) + .await; + let result = (entry.payload)(name.to_string(), args.clone()).await; let output = match &result { Ok(Some(reason)) => json!({ "allowed": false, @@ -816,7 +832,7 @@ impl NemoRelayContextState { "error": error.to_string(), }), }; - Self::emit_guardrail_scope_end(&handle, output, subscribers); + Self::emit_guardrail_scope_end(&handle, output, subscribers).await; if let Some(error) = result? { return Ok(Some(error)); } @@ -859,14 +875,14 @@ impl NemoRelayContextState { /// # Notes /// If an intercept entry has `break_chain` enabled, later intercepts are /// skipped after that entry runs. - pub(crate) fn tool_request_intercepts_snapshot_chain( + pub(crate) async fn tool_request_intercepts_snapshot_chain( name: &str, args: Json, entries: &[Intercept], ) -> crate::error::Result { let mut value = args; for entry in entries { - value = (entry.payload.callable)(name, value)?; + value = (entry.payload.callable)(name.to_string(), value).await?; if entry.payload.break_chain { break; } @@ -976,14 +992,28 @@ impl NemoRelayContextState { /// /// # Returns /// The sanitized [`LlmRequest`] after every provided guardrail has run. - pub(crate) fn llm_sanitize_request_snapshot_chain( + pub(crate) async fn llm_sanitize_request_snapshot_chain( request: LlmRequest, context: LlmSanitizeRequestContext, entries: &[Guardrail], ) -> Option { let mut value = Some(request); for entry in entries { - value = value.and_then(|value| (entry.payload)(value, context.clone())); + if let Some(current) = value.take() { + match (entry.payload)(current.clone(), context.clone()).await { + Ok(next) => value = next, + Err(error) => { + log::error!( + target: "nemo_relay.runtime", + event = "llm_request_sanitizer_failed", + sanitizer = entry.name.as_str(), + preserved_value = "unsanitized_request"; + "LLM request sanitizer failed; preserving the last valid unsanitized request: {error}" + ); + value = Some(current); + } + } + } } value } @@ -1015,14 +1045,28 @@ impl NemoRelayContextState { /// /// # Returns /// The sanitized response payload after every provided guardrail has run. - pub(crate) fn llm_sanitize_response_snapshot_chain( + pub(crate) async fn llm_sanitize_response_snapshot_chain( response: Json, context: LlmSanitizeResponseContext, entries: &[Guardrail], ) -> Option { let mut value = Some(response); for entry in entries { - value = value.and_then(|value| (entry.payload)(value, context.clone())); + if let Some(current) = value.take() { + match (entry.payload)(current.clone(), context.clone()).await { + Ok(next) => value = next, + Err(error) => { + log::error!( + target: "nemo_relay.runtime", + event = "llm_response_sanitizer_failed", + sanitizer = entry.name.as_str(), + preserved_value = "unsanitized_response"; + "LLM response sanitizer failed; preserving the last valid unsanitized response: {error}" + ); + value = Some(current); + } + } + } } value } @@ -1071,7 +1115,7 @@ impl NemoRelayContextState { /// # Errors /// Propagates any error returned by a guardrail callback after emitting the /// corresponding guardrail scope end event. - pub(crate) fn llm_conditional_execution_snapshot_chain( + pub(crate) async fn llm_conditional_execution_snapshot_chain( request: &LlmRequest, entries: &[Guardrail], subscribers: &[EventSubscriberFn], @@ -1087,8 +1131,9 @@ impl NemoRelayContextState { "kind": "llm_conditional_execution", }), subscribers, - ); - let result = (entry.payload)(request); + ) + .await; + let result = (entry.payload)(request.clone()).await; let output = match &result { Ok(Some(reason)) => json!({ "allowed": false, @@ -1104,7 +1149,7 @@ impl NemoRelayContextState { "error": error.to_string(), }), }; - Self::emit_guardrail_scope_end(&handle, output, subscribers); + Self::emit_guardrail_scope_end(&handle, output, subscribers).await; if let Some(error) = result? { return Ok(Some(error)); } @@ -1151,7 +1196,7 @@ impl NemoRelayContextState { /// # Notes /// If an intercept entry has `break_chain` enabled, later intercepts are /// skipped after that entry runs. - pub(crate) fn llm_request_intercepts_snapshot_chain( + pub(crate) async fn llm_request_intercepts_snapshot_chain( name: &str, request: LlmRequest, annotated: Option, @@ -1166,11 +1211,12 @@ impl NemoRelayContextState { codec_active, None, ) + .await } /// Run a request-intercept snapshot while ingesting optimization evidence /// directly into the managed call's bounded accumulator. - pub(crate) fn llm_request_intercepts_snapshot_chain_with_recorder( + pub(crate) async fn llm_request_intercepts_snapshot_chain_with_recorder( name: &str, request: LlmRequest, annotated: Option, @@ -1184,7 +1230,8 @@ impl NemoRelayContextState { let mut optimization_contributions = Vec::new(); for entry in entries { let input_content = request_value.content.clone(); - let outcome = (entry.payload.callable)(name, request_value, annotated_value)?; + let outcome = + (entry.payload.callable)(name.to_string(), request_value, annotated_value).await?; if codec_active && outcome.request.content != input_content { return Err(crate::error::FlowError::InvalidArgument(format!( "LLM request intercept '{}' changed request.content while a request codec is active; modify annotated_request instead", diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index b050a6f45..bdd48d20f 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -9,6 +9,12 @@ use crate::api::runtime::{ EventSanitizeFn, EventSubscriberFn, NemoRelayContextState, ScopeStackHandle, }; use crate::error::Result; +use std::future::Future; +use std::pin::Pin; + +pub(crate) type EventTransformFn = Box< + dyn FnOnce(Event) -> Pin + Send + 'static>> + Send + 'static, +>; mod native { use std::cell::Cell; @@ -27,6 +33,7 @@ mod native { enum DispatcherMessage { Deliver { event: Box, + transform: Option, sanitizers: Vec>, subscribers: Vec, scope_stack: ScopeStackHandle, @@ -34,11 +41,17 @@ mod native { Flush { done: Sender<()>, }, + Barrier { + done: Receiver<()>, + }, } static DISPATCHER: OnceLock, String>> = OnceLock::new(); + static SANITIZER_RUNTIME: OnceLock> = + OnceLock::new(); static DISPATCHER_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); + static SANITIZER_RUNTIME_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); thread_local! { static IN_DISPATCHER: Cell = const { Cell::new(false) }; @@ -50,6 +63,7 @@ mod native { } let message = DispatcherMessage::Deliver { event: Box::new(event.clone()), + transform: None, sanitizers: Vec::new(), subscribers: subscribers.to_vec(), scope_stack: current_scope_stack(), @@ -92,31 +106,41 @@ mod native { } let message = DispatcherMessage::Deliver { event: Box::new(event), + transform: None, sanitizers, subscribers: subscribers.to_vec(), scope_stack, }; - match dispatcher_sender() { - Ok(sender) if sender.send(message).is_ok() => true, - Ok(_) => { - log::warn!( - target: "nemo_relay.runtime", - event = "subscriber_event_dropped", - reason = "dispatcher_disconnected"; - "Subscriber event was dropped because the dispatcher stopped" - ); - false - } - Err(error) if !DISPATCHER_FAILURE_LOGGED.swap(true, Ordering::AcqRel) => { - log::error!( - target: "nemo_relay.runtime", - event = "subscriber_dispatcher_failed"; - "Subscriber dispatcher failed to start: {error}" - ); - false - } - Err(_) => false, - } + send_dispatch_message(message) + } + + pub(super) fn dispatch_transformed_event( + event: Event, + transform: EventTransformFn, + sanitizers: Vec>, + subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, + ) -> bool { + let message = DispatcherMessage::Deliver { + event: Box::new(event), + transform: Some(transform), + sanitizers, + subscribers: subscribers.to_vec(), + scope_stack, + }; + send_dispatch_message(message) + } + + /// Insert a FIFO barrier for work that will enqueue a publication from an + /// async task. A later flush waits for the task to signal completion, then + /// drains the event it queued before acknowledging the flush. + pub(super) fn register_async_publication() -> Option> { + let sender = dispatcher_sender().ok()?; + let (done_tx, done_rx) = mpsc::channel(); + sender + .send(DispatcherMessage::Barrier { done: done_rx }) + .ok() + .map(|_| done_tx) } pub(super) fn flush_subscribers() -> Result<()> { @@ -145,6 +169,30 @@ mod native { DISPATCHER.get_or_init(start_dispatcher).clone() } + fn send_dispatch_message(message: DispatcherMessage) -> bool { + match dispatcher_sender() { + Ok(sender) if sender.send(message).is_ok() => true, + Ok(_) => { + log::warn!( + target: "nemo_relay.runtime", + event = "subscriber_event_dropped", + reason = "dispatcher_disconnected"; + "Subscriber event was dropped because the dispatcher stopped" + ); + false + } + Err(error) if !DISPATCHER_FAILURE_LOGGED.swap(true, Ordering::AcqRel) => { + log::error!( + target: "nemo_relay.runtime", + event = "subscriber_dispatcher_failed"; + "Subscriber dispatcher failed to start: {error}" + ); + false + } + Err(_) => false, + } + } + fn start_dispatcher() -> std::result::Result, String> { let (tx, rx) = mpsc::channel::(); let sender = std::thread::Builder::new() @@ -172,6 +220,9 @@ mod native { let _ = pending.send(()); } } + DispatcherMessage::Barrier { done } => { + let _ = done.recv(); + } message => handle_message(message), } } @@ -182,6 +233,9 @@ mod native { while let Ok(message) = rx.try_recv() { match message { DispatcherMessage::Flush { done } => pending_flushes.push(done), + DispatcherMessage::Barrier { done } => { + let _ = done.recv(); + } message => handle_message(message), } } @@ -192,18 +246,23 @@ mod native { match message { DispatcherMessage::Deliver { event, + transform, sanitizers, subscribers, scope_stack, - } => deliver_event(event, sanitizers, subscribers, scope_stack), + } => deliver_event(event, transform, sanitizers, subscribers, scope_stack), DispatcherMessage::Flush { done } => { let _ = done.send(()); } + DispatcherMessage::Barrier { done } => { + let _ = done.recv(); + } } } fn deliver_event( event: Box, + transform: Option, sanitizers: Vec>, subscribers: Vec, scope_stack: ScopeStackHandle, @@ -211,7 +270,11 @@ mod native { let previous_scope_stack = capture_thread_scope_stack(); set_thread_scope_stack(scope_stack); IN_DISPATCHER.with(|flag| flag.set(true)); - let event = NemoRelayContextState::event_sanitize_snapshot_chain(*event, &sanitizers); + let Some(event) = sanitize_event_snapshot(*event, transform, sanitizers) else { + IN_DISPATCHER.with(|flag| flag.set(false)); + restore_thread_scope_stack(previous_scope_stack); + return; + }; for subscriber in subscribers { if catch_unwind(AssertUnwindSafe(|| subscriber(&event))).is_err() { log::error!( @@ -224,6 +287,75 @@ mod native { IN_DISPATCHER.with(|flag| flag.set(false)); restore_thread_scope_stack(previous_scope_stack); } + + /// Apply a transform and sanitizers on the dispatcher thread. A transform + /// failure drops the event because it may be responsible for inserting the + /// sanitized payload. A sanitizer failure retains the transformed snapshot + /// and continues publication (fail open). + fn sanitize_event_snapshot( + event: Event, + transform: Option, + sanitizers: Vec>, + ) -> Option { + let runtime = match SANITIZER_RUNTIME.get_or_init(|| { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| error.to_string()) + }) { + Ok(runtime) => runtime, + Err(error) => { + if !SANITIZER_RUNTIME_FAILURE_LOGGED.swap(true, Ordering::AcqRel) { + log::error!( + target: "nemo_relay.runtime", + event = "event_sanitizer_runtime_failed"; + "Event sanitizer runtime failed; dropping events: {error}" + ); + } + return None; + } + }; + let transformed = match catch_unwind(AssertUnwindSafe(|| { + runtime.block_on(async move { + match transform { + Some(transform) => transform(event).await, + None => event, + } + }) + })) { + Ok(event) => event, + Err(_) => { + log::error!( + target: "nemo_relay.runtime", + event = "event_transform_panicked"; + "Event transform panicked; dropping the event" + ); + return None; + } + }; + if sanitizers.is_empty() { + return Some(transformed); + } + let original = transformed.clone(); + Some( + match catch_unwind(AssertUnwindSafe(|| { + runtime.block_on(NemoRelayContextState::event_sanitize_snapshot_chain( + transformed, + &sanitizers, + )) + })) { + Ok(event) => event, + Err(_) => { + log::error!( + target: "nemo_relay.runtime", + event = "event_sanitizer_panicked"; + "Event sanitizer panicked; publishing the transformed event snapshot" + ); + original + } + }, + ) + } } /// Queue an event for subscriber delivery. @@ -242,6 +374,26 @@ pub(crate) fn dispatch_sanitized_event( native::dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack) } +/// Queue a snapshot for a middleware-specific asynchronous transformation, +/// followed by event sanitization and subscriber delivery. +pub(crate) fn dispatch_transformed_event( + event: Event, + transform: EventTransformFn, + sanitizers: Vec>, + subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, +) -> bool { + native::dispatch_transformed_event(event, transform, sanitizers, subscribers, scope_stack) +} + +/// Register a FIFO barrier for async work that will queue a subscriber event. +/// +/// Dropping the returned sender releases the barrier, so error paths cannot +/// leave the dispatcher blocked. +pub(crate) fn register_async_publication() -> Option> { + native::register_async_publication() +} + /// Wait for all queued subscriber callbacks submitted before this call. pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() diff --git a/crates/core/src/api/scope.rs b/crates/core/src/api/scope.rs index 27763f836..24e5d8cc3 100644 --- a/crates/core/src/api/scope.rs +++ b/crates/core/src/api/scope.rs @@ -217,9 +217,8 @@ pub fn get_handle() -> Result { /// cannot be read safely. /// /// # Notes -/// The event and its visible middleware/subscriber chains are snapshotted -/// before this function returns. Sanitization and subscriber delivery happen -/// later on the serial publication dispatcher. +/// Scope-local subscribers attached to ancestor scopes observe the emitted +/// start event before the function returns. pub fn push_scope(params: PushScopeParams<'_>) -> Result { ensure_runtime_owner()?; let parent_uuid = resolve_parent_uuid(params.parent); @@ -315,8 +314,9 @@ pub fn pop_scope(params: PopScopeParams<'_>) -> Result<()> { ); (scope, event, subscribers, scope_stack.clone()) }; - // Snapshot scope-local middleware before removing its owner. Publication - // happens later, but cleanup must not change the chain visible at emission. + // Capture the scope-local chain before removing its owner. The event is + // published later, but scope cleanup must not change the middleware that + // was visible when the end event was emitted. let sanitizers = snapshot_event_sanitizers(&event, &emission_scope_stack); let removed = task_scope_remove(params.handle_uuid)?; debug_assert_eq!(removed.uuid, scope.uuid); @@ -353,22 +353,35 @@ pub fn pop_scope(params: PopScopeParams<'_>) -> Result<()> { /// cannot be read safely. /// /// # Notes -/// The event and its visible middleware/subscriber chains are snapshotted -/// before this function returns. Sanitization and subscriber delivery happen -/// later on the serial publication dispatcher. +/// Scope-local subscribers attached to ancestor scopes observe the emitted +/// mark event just like scope, tool, and LLM lifecycle events. pub fn event(params: EmitMarkEventParams<'_>) -> Result<()> { ensure_runtime_owner()?; let parent_uuid = resolve_parent_uuid(params.parent); let scope_stack = current_scope_stack(); let (event, subscribers, emission_scope_stack) = { let subscribers = if params.name == COMPACTION_EVENT_NAME { - let mut scope_guard = scope_stack.write().expect("scope stack lock poisoned"); + let mut scope_guard = scope_stack.write().map_err(|error| { + log::error!( + target: "nemo_relay.runtime", + event = "mark_event_scope_stack_unavailable"; + "Mark event was dropped because the scope stack lock is poisoned: {error}" + ); + FlowError::Internal(error.to_string()) + })?; let subscribers = snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?; scope_guard.mark_agent_fresh(parent_uuid); subscribers } else { - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_guard = scope_stack.read().map_err(|error| { + log::error!( + target: "nemo_relay.runtime", + event = "mark_event_scope_stack_unavailable"; + "Mark event was dropped because the scope stack lock is poisoned: {error}" + ); + FlowError::Internal(error.to_string()) + })?; snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())? }; let context = global_context(); diff --git a/crates/core/src/api/shared.rs b/crates/core/src/api/shared.rs index 92aebeed1..b97c147bb 100644 --- a/crates/core/src/api/shared.rs +++ b/crates/core/src/api/shared.rs @@ -45,36 +45,52 @@ pub(crate) fn snapshot_event_subscribers( } /// Apply the event sanitizer chain visible on the current scope stack. -pub(crate) fn sanitize_event(event: Event) -> Option { - sanitize_event_with_scope_stack(event, ¤t_scope_stack()) +pub(crate) async fn sanitize_event(event: Event) -> Option { + sanitize_event_with_scope_stack(event, ¤t_scope_stack()).await } /// Apply the event sanitizer chain visible on a captured scope stack. -pub(crate) fn sanitize_event_with_scope_stack( +pub(crate) async fn sanitize_event_with_scope_stack( event: Event, scope_stack: &ScopeStackHandle, ) -> Option { let entries = snapshot_event_sanitizers(&event, scope_stack)?; - Some(NemoRelayContextState::event_sanitize_snapshot_chain( - event, &entries, - )) + Some(NemoRelayContextState::event_sanitize_snapshot_chain(event, &entries).await) } -/// Snapshot the event sanitizer chain visible on a captured scope stack. +/// Snapshot the event sanitizers visible to an event without invoking them. /// -/// The snapshot remains valid after the emitting scope is removed, allowing -/// synchronous scope and mark APIs to enqueue publication without changing -/// which scope-local middleware observes the event. +/// Scope and mark emission use this to capture middleware ownership while the +/// scope is still active, then let the serial dispatcher sanitize and publish +/// the immutable event snapshot later. This keeps public scope APIs +/// synchronous while ensuring scope removal cannot affect queued work. pub(crate) fn snapshot_event_sanitizers( event: &Event, scope_stack: &ScopeStackHandle, ) -> Option>> { - Some({ - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let entries = { + let scope_guard = match scope_stack.read() { + Ok(guard) => guard, + Err(error) => { + log::error!( + target: "nemo_relay.runtime", + event = "event_sanitizer_snapshot_failed"; + "Event was dropped because the scope stack lock is poisoned: {error}" + ); + return None; + } + }; let context = global_context(); let state = match context.read() { Ok(state) => state, - Err(_) => return None, + Err(error) => { + log::error!( + target: "nemo_relay.runtime", + event = "event_sanitizer_snapshot_failed"; + "Event was dropped because the runtime context lock is poisoned: {error}" + ); + return None; + } }; match &event { Event::Mark(_) => { @@ -105,7 +121,8 @@ pub(crate) fn snapshot_event_sanitizers( ) } } - }) + }; + Some(entries) } pub(crate) fn ensure_runtime_owner() -> Result<()> { @@ -211,26 +228,26 @@ pub(crate) type InterceptedLlmRequest = ( ); #[cfg(test)] -pub(crate) fn run_request_intercepts_with_codec( +pub(crate) async fn run_request_intercepts_with_codec( name: &str, request: LlmRequest, codec: Option>, ) -> Result { - run_request_intercepts_with_codec_inner(name, request, codec, None) + run_request_intercepts_with_codec_inner(name, request, codec, None).await } /// Run request intercepts and record optimization contributions directly into /// the managed call's bounded accumulator as each intercept completes. -pub(crate) fn run_request_intercepts_with_codec_and_recorder( +pub(crate) async fn run_request_intercepts_with_codec_and_recorder( name: &str, request: LlmRequest, codec: Option>, recorder: &crate::api::optimization::LlmOptimizationRecorder, ) -> Result { - run_request_intercepts_with_codec_inner(name, request, codec, Some(recorder)) + run_request_intercepts_with_codec_inner(name, request, codec, Some(recorder)).await } -fn run_request_intercepts_with_codec_inner( +async fn run_request_intercepts_with_codec_inner( name: &str, request: LlmRequest, codec: Option>, @@ -261,7 +278,8 @@ fn run_request_intercepts_with_codec_inner( &entries, codec.is_some(), recorder, - )?; + ) + .await?; let mut request = outcome.request; inject_dynamo_session_ids(&mut request); let pending_marks = outcome.pending_marks; diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 6fbe6cd70..7d6d10f71 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -7,12 +7,15 @@ use crate::api::event::{BaseEvent, Event, MarkEvent, PendingMarkSpec}; use crate::api::runtime::NemoRelayContextState; use crate::api::runtime::current_scope_stack; use crate::api::runtime::global_context; +use crate::api::runtime::subscriber_dispatcher::{ + dispatch_sanitized_event, dispatch_transformed_event, +}; use crate::api::runtime::{EventSubscriberFn, ToolExecutionNextFn, with_active_event_uuid}; use crate::api::scope::event; use crate::api::scope::{EmitMarkEventParams, ScopeHandle}; use crate::api::shared::{ ensure_runtime_owner, metadata_with_otel_status, resolve_parent_uuid, sanitize_event, - snapshot_event_subscribers, + snapshot_event_sanitizers, snapshot_event_subscribers, }; use crate::api::skill_load; use crate::error::{FlowError, Result}; @@ -206,11 +209,107 @@ pub struct ToolCallEndParams<'a> { /// Sanitize-request guardrails affect only the emitted start-event payload, not /// the caller-owned `args` value. pub fn tool_call(params: ToolCallParams<'_>) -> Result { - let (handle, _) = tool_call_with_subscriber_snapshot(params)?; + ensure_runtime_owner()?; + let scope_stack = current_scope_stack(); + let (entries, subscribers) = { + let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_locals = scope_guard.collect_scope_local_registries(|registries| { + ®istries.tool_sanitize_request_guardrails + }); + let subscribers = + snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?; + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + ( + state.tool_sanitize_request_entries(&scope_locals), + subscribers, + ) + }; + let handled_skill_loads = params + .metadata + .as_ref() + .and_then(Json::as_object) + .and_then(|metadata| metadata.get(skill_load::HANDLED_METADATA_KEY)) + .and_then(Json::as_bool) + .is_some_and(|handled| handled); + let skill_loads = if handled_skill_loads { + Vec::new() + } else if let Some(skill_loads) = skill_load::precomputed(params.metadata.as_ref()) { + skill_loads + } else { + skill_load::detect(params.name, ¶ms.args) + }; + let raw_args = params.args; + let (handle, event, marks) = { + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + let handle = state.create_tool_handle( + CreateToolHandleParams::builder() + .name(params.name) + .parent_uuid_opt(resolve_parent_uuid(params.parent)) + .attributes(params.attributes) + .data_opt(params.data) + .metadata_opt(params.metadata) + .tool_call_id_opt(params.tool_call_id) + .timestamp_opt(params.timestamp) + .build(), + ); + let event = state.build_tool_start_event(&handle, None); + let marks = skill_loads + .into_iter() + .map(|skill_load| { + state.create_event(MarkEvent::new( + BaseEvent::builder() + .name("skill.load") + .parent_uuid(handle.uuid) + .timestamp(handle.started_at) + .data(json!({"skill_name": skill_load.name})) + .metadata(json!({ + "skill_load_source": <&str>::from(skill_load.source), + "tool_name": handle.name, + })) + .build(), + None, + None, + )) + }) + .collect::>(); + (handle, event, marks) + }; + let tool_name = handle.name.clone(); + if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { + dispatch_transformed_event( + event, + Box::new(move |mut event| { + Box::pin(async move { + let sanitized = NemoRelayContextState::tool_sanitize_request_snapshot_chain( + &tool_name, raw_args, &entries, + ) + .await; + let mut fields = event.sanitize_fields(); + fields.data = Some(sanitized); + event.apply_sanitize_fields(fields); + event + }) + }), + event_sanitizers, + &subscribers, + scope_stack.clone(), + ); + } + for mark in marks { + if let Some(sanitizers) = snapshot_event_sanitizers(&mark, &scope_stack) { + dispatch_sanitized_event(mark, sanitizers, &subscribers, scope_stack.clone()); + } + } Ok(handle) } -fn tool_call_with_subscriber_snapshot( +async fn tool_call_with_subscriber_snapshot( params: ToolCallParams<'_>, ) -> Result<(ToolHandle, Vec)> { ensure_runtime_owner()?; @@ -248,7 +347,8 @@ fn tool_call_with_subscriber_snapshot( params.name, params.args, &entries, - ); + ) + .await; let (handle, event, marks) = { let context = global_context(); let state = context @@ -286,14 +386,16 @@ fn tool_call_with_subscriber_snapshot( .collect::>(); (handle, event, marks) }; - let marks = marks - .into_iter() - .filter_map(sanitize_event) - .collect::>(); - if let Some(event) = sanitize_event(event) { + let mut sanitized_marks = Vec::with_capacity(marks.len()); + for mark in marks { + if let Some(mark) = sanitize_event(mark).await { + sanitized_marks.push(mark); + } + } + if let Some(event) = sanitize_event(event).await { NemoRelayContextState::emit_event(&event, &subscribers); } - for mark in marks { + for mark in sanitized_marks { NemoRelayContextState::emit_event(&mark, &subscribers); } Ok((handle, subscribers)) @@ -326,10 +428,69 @@ fn tool_call_with_subscriber_snapshot( /// Sanitize-response guardrails affect only the emitted end-event payload, not /// the caller-owned `result` value. pub fn tool_call_end(params: ToolCallEndParams<'_>) -> Result<()> { - tool_call_end_with_pending_marks(params, Vec::new(), None) + ensure_runtime_owner()?; + let scope_stack = current_scope_stack(); + let (entries, subscribers) = { + let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_locals = scope_guard.collect_scope_local_registries(|registries| { + ®istries.tool_sanitize_response_guardrails + }); + let subscribers = + snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?; + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + ( + state.tool_sanitize_response_entries(&scope_locals), + subscribers, + ) + }; + let result = params.result; + let fallback = params.data; + let event = { + let context = global_context(); + let state = context + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; + state.build_tool_end_event( + EndToolHandleParams::builder() + .handle(params.handle) + .data(Json::Null) + .metadata_opt(params.metadata) + .timestamp_opt(params.timestamp) + .build(), + ) + }; + let tool_name = params.handle.name.clone(); + if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { + dispatch_transformed_event( + event, + Box::new(move |mut event| { + Box::pin(async move { + let sanitized = NemoRelayContextState::tool_sanitize_response_snapshot_chain( + &tool_name, result, &entries, + ) + .await; + let mut fields = event.sanitize_fields(); + fields.data = if sanitized.is_null() { + fallback + } else { + Some(sanitized) + }; + event.apply_sanitize_fields(fields); + event + }) + }), + event_sanitizers, + &subscribers, + scope_stack, + ); + } + Ok(()) } -fn tool_call_end_with_pending_marks( +async fn tool_call_end_with_pending_marks( params: ToolCallEndParams<'_>, pending_marks: Vec, lifecycle_subscribers: Option<&[EventSubscriberFn]>, @@ -358,7 +519,8 @@ fn tool_call_end_with_pending_marks( ¶ms.handle.name, params.result, &entries, - ); + ) + .await; let data = if sanitized_result.is_null() { params.data } else { @@ -396,18 +558,23 @@ fn tool_call_end_with_pending_marks( mark.category_profile, )) }) - .filter_map(sanitize_event) .collect::>(); - if let Some(event) = sanitize_event(event) { + let mut sanitized_marks = Vec::with_capacity(marks.len()); + for mark in marks { + if let Some(mark) = sanitize_event(mark).await { + sanitized_marks.push(mark); + } + } + if let Some(event) = sanitize_event(event).await { NemoRelayContextState::emit_event(&event, subscribers); } - for mark in marks { + for mark in sanitized_marks { NemoRelayContextState::emit_event(&mark, subscribers); } Ok(()) } -fn emit_tool_end_without_output( +async fn emit_tool_end_without_output( handle: &ToolHandle, metadata: Option, lifecycle_subscribers: &[EventSubscriberFn], @@ -420,7 +587,7 @@ fn emit_tool_end_without_output( .map_err(|error| FlowError::Internal(error.to_string()))?; state.end_tool_handle(handle, handle.data.clone(), metadata) }; - if let Some(event) = sanitize_event(event) { + if let Some(event) = sanitize_event(event).await { NemoRelayContextState::emit_event(&event, lifecycle_subscribers); } Ok(()) @@ -493,7 +660,9 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { &subscribers, parent_uuid, guardrail_metadata, - )? { + ) + .await? + { let mut rejection_data = json!({}); if let Some(object) = rejection_data.as_object_mut() { object.insert("rejected".into(), json!(true)); @@ -526,7 +695,8 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { &name, args, &intercept_entries, - )?; + ) + .await?; let (handle, lifecycle_subscribers) = tool_call_with_subscriber_snapshot( ToolCallParams::builder() @@ -537,7 +707,8 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { .data_opt(data.clone()) .metadata_opt(metadata.clone()) .build(), - )?; + ) + .await?; let execution = { let scope_stack = current_scope_stack(); @@ -567,13 +738,15 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { .build(), pending_marks, Some(&lifecycle_subscribers), - )?; + ) + .await?; Ok(result) } Err(error) => { let end_metadata = metadata_with_otel_status(metadata, "ERROR", Some(error.to_string())); - let _ = emit_tool_end_without_output(&handle, end_metadata, &lifecycle_subscribers); + let _ = + emit_tool_end_without_output(&handle, end_metadata, &lifecycle_subscribers).await; Err(error) } } @@ -596,7 +769,7 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { /// /// # Notes /// Conditional guardrails and execution intercepts are not run by this helper. -pub fn tool_request_intercepts(name: &str, args: Json) -> Result { +pub async fn tool_request_intercepts(name: &str, args: Json) -> Result { ensure_runtime_owner()?; let entries = { let scope_stack = current_scope_stack(); @@ -609,7 +782,7 @@ pub fn tool_request_intercepts(name: &str, args: Json) -> Result { .map_err(|error| FlowError::Internal(error.to_string()))?; state.tool_request_intercept_entries(&scope_locals) }; - NemoRelayContextState::tool_request_intercepts_snapshot_chain(name, args, &entries) + NemoRelayContextState::tool_request_intercepts_snapshot_chain(name, args, &entries).await } /// Run only the tool conditional-execution guardrail chain. @@ -633,7 +806,7 @@ pub fn tool_request_intercepts(name: &str, args: Json) -> Result { /// This helper is useful for preflight checks when the caller needs the /// rejection result without starting a tool span. Guardrail scopes are still /// emitted for the conditional checks themselves. -pub fn tool_conditional_execution(name: &str, args: &Json) -> Result<()> { +pub async fn tool_conditional_execution(name: &str, args: &Json) -> Result<()> { ensure_runtime_owner()?; let (entries, subscribers, parent_uuid) = { let scope_stack = current_scope_stack(); @@ -657,7 +830,9 @@ pub fn tool_conditional_execution(name: &str, args: &Json) -> Result<()> { &subscribers, parent_uuid, None, - )? { + ) + .await? + { return Err(FlowError::GuardrailRejected(error)); } Ok(()) diff --git a/crates/core/src/logging/rotation.rs b/crates/core/src/logging/rotation.rs index 3314377c1..fa55780bb 100644 --- a/crates/core/src/logging/rotation.rs +++ b/crates/core/src/logging/rotation.rs @@ -146,3 +146,7 @@ pub(crate) fn rotated_log_path(base_path: &Path, index: usize) -> PathBuf { } base_path.with_file_name(file_name) } + +#[cfg(test)] +#[path = "../../tests/coverage/logging_rotation_tests.rs"] +mod tests; diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index f1217c746..7481d86ee 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -14,24 +14,27 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::ptr; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use std::task::{Context, Poll}; use chrono::{DateTime, Utc}; use libloading::{Library, Symbol}; use nemo_relay_plugin::{ - NEMO_RELAY_NATIVE_ABI_VERSION, NemoRelayNativeEventSanitizeCb, - NemoRelayNativeEventSubscriberCb, NemoRelayNativeFreeFn, NemoRelayNativeHostApiV1, - NemoRelayNativeLlmCodecKind, NemoRelayNativeLlmConditionalCb, NemoRelayNativeLlmExecutionCb, - NemoRelayNativeLlmRequestCodec, NemoRelayNativeLlmRequestInterceptCb, - NemoRelayNativeLlmResponseCodec, NemoRelayNativeLlmSanitizeRequestCb, - NemoRelayNativeLlmSanitizeRequestContext, NemoRelayNativeLlmSanitizeResponseCb, - NemoRelayNativeLlmSanitizeResponseContext, NemoRelayNativeLlmStreamExecutionCb, - NemoRelayNativeLlmStreamV1, NemoRelayNativePluginContext, NemoRelayNativePluginEntry, - NemoRelayNativePluginV1, NemoRelayNativeScopeHandle, NemoRelayNativeScopeStack, - NemoRelayNativeScopeStackBinding, NemoRelayNativeScopeType, NemoRelayNativeString, - NemoRelayNativeToolConditionalCb, NemoRelayNativeToolExecutionCb, NemoRelayNativeToolJsonCb, - NemoRelayNativeWithScopeStackCb, NemoRelayStatus, + NEMO_RELAY_NATIVE_ABI_VERSION, NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY, + NemoRelayNativeAsyncCallbackState, NemoRelayNativeAsyncCompletion, + NemoRelayNativeAsyncMiddlewareCb, NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativeAsyncNext, + NemoRelayNativeEventSanitizeCb, NemoRelayNativeEventSubscriberCb, NemoRelayNativeFreeFn, + NemoRelayNativeHostApiV1, NemoRelayNativeHostApiV3, NemoRelayNativeLlmCodecKind, + NemoRelayNativeLlmConditionalCb, NemoRelayNativeLlmExecutionCb, NemoRelayNativeLlmRequestCodec, + NemoRelayNativeLlmRequestInterceptCb, NemoRelayNativeLlmResponseCodec, + NemoRelayNativeLlmSanitizeRequestCb, NemoRelayNativeLlmSanitizeRequestContext, + NemoRelayNativeLlmSanitizeResponseCb, NemoRelayNativeLlmSanitizeResponseContext, + NemoRelayNativeLlmStreamExecutionCb, NemoRelayNativeLlmStreamV1, NemoRelayNativePluginContext, + NemoRelayNativePluginEntry, NemoRelayNativePluginV1, NemoRelayNativeScopeHandle, + NemoRelayNativeScopeStack, NemoRelayNativeScopeStackBinding, NemoRelayNativeScopeType, + NemoRelayNativeString, NemoRelayNativeToolConditionalCb, NemoRelayNativeToolExecutionCb, + NemoRelayNativeToolJsonCb, NemoRelayNativeWithScopeStackCb, NemoRelayStatus, }; use semver::{Version, VersionReq}; use serde_json::{Map, Value as Json}; @@ -374,7 +377,14 @@ fn load_one_native_plugin( library_path.display() )) })?; - let status = entry(native_host_api(), &mut plugin); + let mut status = entry(native_host_api(), &mut plugin); + // SDKs compiled against ABI v2 correctly reject a v3 table. Retry + // their entry point with the frozen v2 prefix instead of making a + // runtime upgrade a breaking change for installed native plugins. + if status == NemoRelayStatus::InvalidArg { + drop_native_plugin_descriptor(&mut plugin); + status = entry(native_host_api_legacy(), &mut plugin); + } if status != NemoRelayStatus::Ok { drop_native_plugin_descriptor(&mut plugin); return Err(PluginError::RegistrationFailed(format!( @@ -784,10 +794,19 @@ unsafe extern "C" fn native_llm_response_codec_decode( } fn native_host_api() -> *const NemoRelayNativeHostApiV1 { + static HOST_API: OnceLock = OnceLock::new(); + &HOST_API.get_or_init(build_native_host_api_v3).v1 as *const NemoRelayNativeHostApiV1 +} + +fn native_host_api_legacy() -> *const NemoRelayNativeHostApiV1 { static HOST_API: OnceLock = OnceLock::new(); + HOST_API.get_or_init(build_native_host_api_legacy) as *const _ +} + +fn build_native_host_api_legacy() -> NemoRelayNativeHostApiV1 { static RELAY_VERSION: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes(); - HOST_API.get_or_init(|| NemoRelayNativeHostApiV1 { - abi_version: NEMO_RELAY_NATIVE_ABI_VERSION, + NemoRelayNativeHostApiV1 { + abi_version: NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY, struct_size: std::mem::size_of::(), relay_version: RELAY_VERSION.as_ptr().cast(), string_new: native_string_new, @@ -841,7 +860,23 @@ fn native_host_api() -> *const NemoRelayNativeHostApiV1 { native_plugin_context_register_scope_sanitize_start_guardrail, plugin_context_register_scope_sanitize_end_guardrail: native_plugin_context_register_scope_sanitize_end_guardrail, - }) as *const _ + } +} + +fn build_native_host_api_v3() -> NemoRelayNativeHostApiV3 { + let mut v1 = build_native_host_api_legacy(); + v1.abi_version = NEMO_RELAY_NATIVE_ABI_VERSION; + v1.struct_size = std::mem::size_of::(); + NemoRelayNativeHostApiV3 { + v1, + async_completion_resolve_json: native_async_completion_resolve_json, + async_completion_reject: native_async_completion_reject, + async_completion_is_cancelled: native_async_completion_is_cancelled, + async_completion_release: native_async_completion_release, + async_next_invoke: native_async_next_invoke, + async_next_release: native_async_next_release, + plugin_context_register_async_middleware: native_plugin_context_register_async_middleware, + } } fn read_native_string(value: *const NemoRelayNativeString) -> crate::plugin::Result { @@ -1305,6 +1340,680 @@ fn make_user_data( }) } +/// One-shot state retained by a v3 native async callback. +struct NativeAsyncCompletion { + sender: Mutex>>>, + cancelled: AtomicBool, + // A pending native callback can continue running after its completion + // wakes the awaiting task. Keep the callback's dynamic-library instance + // alive until native code explicitly releases this handle. + _callback_user_data: Option>, +} + +struct NativeAsyncWait { + completion: Arc, + receiver: tokio::sync::oneshot::Receiver>, +} + +impl Drop for NativeAsyncWait { + fn drop(&mut self) { + self.completion.cancelled.store(true, Ordering::Release); + } +} + +enum NativeAsyncNextInner { + Tool(ToolExecutionNextFn), + Llm(LlmExecutionNextFn), + LlmStream(LlmStreamExecutionNextFn), +} + +struct NativeAsyncNext { + inner: NativeAsyncNextInner, + runtime: tokio::runtime::Handle, + // The native callback owns this handle independently of its completion. + // Retaining the library here prevents an unload while it still uses `next`. + _callback_user_data: Option>, +} + +async fn invoke_native_async_callback( + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: Arc, + invocation: Json, + next: Option, +) -> FlowResult { + let runtime = if next.is_some() { + Some(tokio::runtime::Handle::try_current().map_err(|error| { + FlowError::Internal(format!( + "native async intercept requires a Tokio runtime: {error}" + )) + })?) + } else { + None + }; + let invocation = native_string_from_json(&invocation) + .ok_or_else(|| FlowError::Internal("failed to allocate native async invocation".into()))? + as usize; + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + _callback_user_data: Some(user_data.clone()), + }); + let completion_ref = Arc::into_raw(completion.clone()) as usize; + let next_ref = match (next, runtime) { + (Some(inner), Some(runtime)) => Some(Arc::into_raw(Arc::new(NativeAsyncNext { + inner, + runtime, + _callback_user_data: Some(user_data.clone()), + })) as usize), + (None, None) => None, + _ => unreachable!("runtime is present exactly for native async intercepts"), + }; + let state = match catch_unwind(AssertUnwindSafe(|| unsafe { + cb( + user_data.ptr, + invocation as *const NemoRelayNativeString, + next_ref + .map(|next| next as *const NemoRelayNativeAsyncNext) + .unwrap_or(ptr::null()), + completion_ref as *const NemoRelayNativeAsyncCompletion, + ) + })) { + Ok(state) => state, + Err(_) => { + unsafe { + drop(Arc::from_raw( + completion_ref as *const NativeAsyncCompletion, + )); + if let Some(next_ref) = next_ref { + drop(Arc::from_raw(next_ref as *const NativeAsyncNext)); + } + native_string_free(invocation as *mut NemoRelayNativeString); + } + return Err(FlowError::Internal("native async callback panicked".into())); + } + }; + unsafe { native_string_free(invocation as *mut NemoRelayNativeString) }; + if state == NemoRelayNativeAsyncCallbackState::Complete { + unsafe { + drop(Arc::from_raw( + completion_ref as *const NativeAsyncCompletion, + )); + if let Some(next_ref) = next_ref { + drop(Arc::from_raw(next_ref as *const NativeAsyncNext)); + } + } + if completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_some() + { + return Err(FlowError::Internal( + "native async callback returned Complete without settling".into(), + )); + } + } + let mut wait = NativeAsyncWait { + completion, + receiver, + }; + (&mut wait.receiver) + .await + .map_err(|_| FlowError::Internal("native async callback dropped without settling".into()))? +} + +unsafe extern "C" fn native_async_completion_resolve_json( + completion: *const NemoRelayNativeAsyncCompletion, + value_json: *const NemoRelayNativeString, +) -> NemoRelayStatus { + let Some(completion) = (unsafe { (completion as *const NativeAsyncCompletion).as_ref() }) + else { + return NemoRelayStatus::NullPointer; + }; + if completion.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } + let value = match parse_json_arg(value_json, "native async completion result") { + Ok(value) => value, + Err(status) => return status, + }; + let Some(sender) = completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + else { + return NemoRelayStatus::InvalidArg; + }; + let _ = sender.send(Ok(value)); + NemoRelayStatus::Ok +} + +unsafe extern "C" fn native_async_completion_reject( + completion: *const NemoRelayNativeAsyncCompletion, + message: *const NemoRelayNativeString, +) -> NemoRelayStatus { + let Some(completion) = (unsafe { (completion as *const NativeAsyncCompletion).as_ref() }) + else { + return NemoRelayStatus::NullPointer; + }; + if completion.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } + let message = if message.is_null() { + "native async callback rejected".to_string() + } else { + match read_native_string(message) { + Ok(message) => message, + Err(error) => { + set_native_last_error(error.to_string()); + return NemoRelayStatus::InvalidArg; + } + } + }; + let Some(sender) = completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + else { + return NemoRelayStatus::InvalidArg; + }; + let _ = sender.send(Err(FlowError::Internal(message))); + NemoRelayStatus::Ok +} + +unsafe extern "C" fn native_async_completion_is_cancelled( + completion: *const NemoRelayNativeAsyncCompletion, +) -> bool { + unsafe { (completion as *const NativeAsyncCompletion).as_ref() } + .is_none_or(|completion| completion.cancelled.load(Ordering::Acquire)) +} + +unsafe extern "C" fn native_async_completion_release( + completion: *const NemoRelayNativeAsyncCompletion, +) { + if !completion.is_null() { + unsafe { drop(Arc::from_raw(completion as *const NativeAsyncCompletion)) }; + } +} + +unsafe extern "C" fn native_async_next_release(next: *const NemoRelayNativeAsyncNext) { + if !next.is_null() { + unsafe { drop(Arc::from_raw(next as *const NativeAsyncNext)) }; + } +} + +/// Invokes the runtime continuation without blocking the calling native thread. +unsafe extern "C" fn native_async_next_invoke( + next: *const NemoRelayNativeAsyncNext, + invocation_json: *const NemoRelayNativeString, + completion: *const NemoRelayNativeAsyncCompletion, +) -> NemoRelayStatus { + let Some(next) = (unsafe { (next as *const NativeAsyncNext).as_ref() }) else { + return NemoRelayStatus::NullPointer; + }; + if completion.is_null() { + return NemoRelayStatus::NullPointer; + } + let invocation = match parse_json_arg(invocation_json, "native async next invocation") { + Ok(value) => value, + Err(status) => return status, + }; + unsafe { Arc::increment_strong_count(completion as *const NativeAsyncCompletion) }; + let completion = unsafe { Arc::from_raw(completion as *const NativeAsyncCompletion) }; + let future: Pin> + Send>> = match &next.inner { + NativeAsyncNextInner::Tool(next) => { + let next = next.clone(); + Box::pin(async move { + serde_json::to_value(ToolExecutionInterceptOutcome::new(next(invocation).await?)) + .map_err(|error| { + FlowError::Internal(format!( + "failed to serialize native async tool outcome: {error}" + )) + }) + }) + } + NativeAsyncNextInner::Llm(next) => { + let request = match serde_json::from_value(invocation) { + Ok(request) => request, + Err(error) => { + let _ = completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + .map(|sender| sender.send(Err(FlowError::Internal(error.to_string())))); + return NemoRelayStatus::InvalidArg; + } + }; + let next = next.clone(); + Box::pin(async move { next(request).await }) + } + NativeAsyncNextInner::LlmStream(next) => { + let request = match serde_json::from_value(invocation) { + Ok(request) => request, + Err(error) => { + let _ = completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + .map(|sender| sender.send(Err(FlowError::Internal(error.to_string())))); + return NemoRelayStatus::InvalidArg; + } + }; + let next = next.clone(); + Box::pin(async move { + let mut stream = next(request).await?; + let mut chunks = Vec::new(); + while let Some(chunk) = stream.next().await { + chunks.push(chunk?); + } + Ok(Json::Array(chunks)) + }) + } + }; + next.runtime.spawn(async move { + let result = future.await; + if let Some(sender) = completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + let _ = sender.send(result); + } + }); + NemoRelayStatus::Ok +} + +fn wrap_native_async_tool_json( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> ToolSanitizeFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |name, value| { + let user_data = user_data.clone(); + Box::pin(async move { + let value = invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"name": name, "value": value}), + None, + ) + .await?; + Ok(value) + }) + }) +} + +fn wrap_native_async_tool_conditional( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> ToolConditionalFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |name, value| { + let user_data = user_data.clone(); + Box::pin(async move { + match invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"name": name, "value": value}), + None, + ) + .await? + { + Json::Null => Ok(None), + Json::String(reason) => Ok(Some(reason)), + other => Err(FlowError::Internal(format!( + "native async tool conditional callback returned {other}; expected string or null" + ))), + } + }) + }) +} + +fn wrap_native_async_llm_conditional( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> LlmConditionalFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |request| { + let user_data = user_data.clone(); + Box::pin(async move { + match invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"request": request}), + None, + ) + .await? + { + Json::Null => Ok(None), + Json::String(reason) => Ok(Some(reason)), + other => Err(FlowError::Internal(format!( + "native async LLM conditional callback returned {other}; expected string or null" + ))), + } + }) + }) +} + +fn wrap_native_async_llm_sanitize_request( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> LlmSanitizeRequestFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |request, context| { + let user_data = user_data.clone(); + let codec = format!("{:?}", context.codec()); + Box::pin(async move { + let value = invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"request": request, "context": {"codec": codec}}), + None, + ) + .await?; + if value.is_null() { + Ok(None) + } else { + serde_json::from_value(value) + .map(Some) + .map_err(|error| FlowError::Internal(error.to_string())) + } + }) + }) +} + +fn wrap_native_async_llm_sanitize_response( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> LlmSanitizeResponseFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |response, context| { + let user_data = user_data.clone(); + let codec = format!("{:?}", context.codec()); + Box::pin(async move { + let value = invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"response": response, "context": {"codec": codec}}), + None, + ) + .await?; + Ok((!value.is_null()).then_some(value)) + }) + }) +} + +fn wrap_native_async_llm_request_intercept( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> LlmRequestInterceptFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |name, request, annotated| { + let user_data = user_data.clone(); + Box::pin(async move { + serde_json::from_value( + invoke_native_async_callback( + cb, + user_data, + serde_json::json!({ + "name": name, + "request": request, + "annotated": annotated, + }), + None, + ) + .await?, + ) + .map_err(|error| { + FlowError::Internal(format!( + "invalid native async LLM intercept outcome: {error}" + )) + }) + }) + }) +} + +fn wrap_native_async_event_sanitize( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> EventSanitizeFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |event, fields| { + let user_data = user_data.clone(); + Box::pin(async move { + serde_json::from_value( + invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"event": event, "fields": fields}), + None, + ) + .await?, + ) + .map_err(|error| { + FlowError::Internal(format!("invalid native async event fields: {error}")) + }) + }) + }) +} + +fn wrap_native_async_tool_execution( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> ToolExecutionFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |name, args, next| { + let user_data = user_data.clone(); + let invocation = serde_json::json!({"name": name, "value": args}); + Box::pin(async move { + serde_json::from_value( + invoke_native_async_callback( + cb, + user_data, + invocation, + Some(NativeAsyncNextInner::Tool(next)), + ) + .await?, + ) + .map_err(|error| { + FlowError::Internal(format!("invalid native async tool outcome: {error}")) + }) + }) + }) +} + +fn wrap_native_async_llm_execution( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> LlmExecutionFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |name, request, next| { + let user_data = user_data.clone(); + Box::pin(invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"name": name, "request": request}), + Some(NativeAsyncNextInner::Llm(next)), + )) + }) +} + +fn wrap_native_async_llm_stream_execution( + instance: Arc, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> LlmStreamExecutionFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |name, request, next| { + let user_data = user_data.clone(); + let name = name.to_owned(); + Box::pin(async move { + let value = invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"name": name, "request": request}), + Some(NativeAsyncNextInner::LlmStream(next)), + ) + .await?; + let chunks = value.as_array().cloned().ok_or_else(|| { + FlowError::Internal( + "native async LLM stream intercept must resolve to an array".into(), + ) + })?; + Ok(LlmJsonStream::new(tokio_stream::iter( + chunks.into_iter().map(Ok), + ))) + }) + }) +} + +unsafe extern "C" fn native_plugin_context_register_async_middleware( + ctx: *mut NemoRelayNativePluginContext, + kind: NemoRelayNativeAsyncMiddlewareKind, + name: *const NemoRelayNativeString, + priority: i32, + break_chain: bool, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> NemoRelayStatus { + clear_native_last_error(); + let host_ctx = match host_ctx_mut(ctx) { + Ok(ctx) => ctx, + Err(status) => return status, + }; + let instance = host_ctx.instance.clone(); + let name = match read_name(name) { + Ok(name) => name, + Err(status) => return status, + }; + let context = unsafe { &mut *host_ctx.ctx }; + let registration = match kind { + NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest => context + .register_tool_sanitize_request_guardrail( + &name, + priority, + wrap_native_async_tool_json(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse => context + .register_tool_sanitize_response_guardrail( + &name, + priority, + wrap_native_async_tool_json(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution => context + .register_tool_conditional_execution_guardrail( + &name, + priority, + wrap_native_async_tool_conditional(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept => context + .register_tool_request_intercept( + &name, + priority, + break_chain, + wrap_native_async_tool_json(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept => context + .register_tool_execution_intercept( + &name, + priority, + wrap_native_async_tool_execution(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest => context + .register_llm_sanitize_request_guardrail( + &name, + priority, + wrap_native_async_llm_sanitize_request(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse => context + .register_llm_sanitize_response_guardrail( + &name, + priority, + wrap_native_async_llm_sanitize_response(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution => context + .register_llm_conditional_execution_guardrail( + &name, + priority, + wrap_native_async_llm_conditional(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept => { + if let Err(error) = validate_annotated_request_consumer_compatibility( + &instance.relay_compat, + &instance.plugin_kind, + ) { + return status_from_plugin_error(error); + } + context.register_llm_request_intercept( + &name, + priority, + break_chain, + wrap_native_async_llm_request_intercept(instance, cb, user_data, free_fn), + ) + } + NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept => context + .register_llm_execution_intercept( + &name, + priority, + wrap_native_async_llm_execution(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept => context + .register_llm_stream_execution_intercept( + &name, + priority, + wrap_native_async_llm_stream_execution(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::MarkSanitize => context + .register_mark_sanitize_guardrail( + &name, + priority, + wrap_native_async_event_sanitize(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart => context + .register_scope_sanitize_start_guardrail( + &name, + priority, + wrap_native_async_event_sanitize(instance, cb, user_data, free_fn), + ), + NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd => context + .register_scope_sanitize_end_guardrail( + &name, + priority, + wrap_native_async_event_sanitize(instance, cb, user_data, free_fn), + ), + }; + match registration { + Ok(()) => NemoRelayStatus::Ok, + Err(error) => status_from_plugin_error(error), + } +} + fn host_ctx_mut<'a>( ctx: *mut NemoRelayNativePluginContext, ) -> Result<&'a mut NativeHostPluginContext, NemoRelayStatus> { @@ -1740,7 +2449,8 @@ fn wrap_event_sanitize_fn( ) -> EventSanitizeFn { let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |event, fields| { - call_event_sanitize_callback(cb, user_data.ptr, event, &fields).unwrap_or_default() + let user_data = user_data.clone(); + Box::pin(async move { call_event_sanitize_callback(cb, user_data.ptr, &event, &fields) }) }) } @@ -1795,7 +2505,8 @@ fn wrap_tool_json_fn( ) -> ToolSanitizeFn { let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |name, payload| { - call_tool_json_callback(cb, user_data.ptr, name, &payload).unwrap_or(Json::Null) + let user_data = user_data.clone(); + Box::pin(async move { call_tool_json_callback(cb, user_data.ptr, &name, &payload) }) }) } @@ -1806,7 +2517,10 @@ fn wrap_tool_intercept_fn( free_fn: NemoRelayNativeFreeFn, ) -> ToolInterceptFn { let user_data = make_user_data(instance, user_data, free_fn); - Arc::new(move |name, payload| call_tool_json_callback(cb, user_data.ptr, name, &payload)) + Arc::new(move |name, payload| { + let user_data = user_data.clone(); + Box::pin(async move { call_tool_json_callback(cb, user_data.ptr, &name, &payload) }) + }) } fn call_tool_json_callback( @@ -1846,32 +2560,35 @@ fn wrap_tool_conditional_fn( ) -> ToolConditionalFn { let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |name, args| { - clear_native_last_error(); - let name_string = native_string_from_str(name) - .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?; - let args_string = native_string_from_json(args) - .ok_or_else(|| FlowError::Internal("failed to allocate native args".into()))?; - let mut out = ptr::null_mut(); - let status = unsafe { cb(user_data.ptr, name_string, args_string, &mut out) }; - unsafe { - native_string_free(name_string); - native_string_free(args_string); - } - if status != NemoRelayStatus::Ok { - if !out.is_null() { - unsafe { native_string_free(out) }; + let user_data = user_data.clone(); + Box::pin(async move { + clear_native_last_error(); + let name_string = native_string_from_str(&name) + .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?; + let args_string = native_string_from_json(&args) + .ok_or_else(|| FlowError::Internal("failed to allocate native args".into()))?; + let mut out = ptr::null_mut(); + let status = unsafe { cb(user_data.ptr, name_string, args_string, &mut out) }; + unsafe { + native_string_free(name_string); + native_string_free(args_string); } - return Err(flow_error_from_status( - status, - "native tool conditional failed", - )); - } - if out.is_null() { - Ok(None) - } else { - let reason = take_native_string(out)?; - Ok(Some(reason)) - } + if status != NemoRelayStatus::Ok { + if !out.is_null() { + unsafe { native_string_free(out) }; + } + return Err(flow_error_from_status( + status, + "native tool conditional failed", + )); + } + if out.is_null() { + Ok(None) + } else { + let reason = take_native_string(out)?; + Ok(Some(reason)) + } + }) }) } @@ -1961,9 +2678,10 @@ fn wrap_llm_sanitize_request_fn( ) -> LlmSanitizeRequestFn { let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |request, context| { - call_llm_sanitize_request_callback(cb, user_data.ptr, &request, context) - .ok() - .flatten() + let user_data = user_data.clone(); + Box::pin( + async move { call_llm_sanitize_request_callback(cb, user_data.ptr, &request, context) }, + ) }) } @@ -1975,9 +2693,10 @@ fn wrap_llm_sanitize_response_fn( ) -> LlmSanitizeResponseFn { let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |payload, context| { - call_llm_sanitize_response_callback(cb, user_data.ptr, &payload, context) - .ok() - .flatten() + let user_data = user_data.clone(); + Box::pin(async move { + call_llm_sanitize_response_callback(cb, user_data.ptr, &payload, context) + }) }) } @@ -2118,30 +2837,34 @@ fn wrap_llm_conditional_fn( ) -> LlmConditionalFn { let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |request| { - clear_native_last_error(); - let request_json = serde_json::to_value(request).map_err(|err| { - FlowError::Internal(format!("failed to serialize LLM request: {err}")) - })?; - let request_string = native_string_from_json(&request_json) - .ok_or_else(|| FlowError::Internal("failed to allocate native LLM request".into()))?; - let mut out = ptr::null_mut(); - let status = unsafe { cb(user_data.ptr, request_string, &mut out) }; - unsafe { native_string_free(request_string) }; - if status != NemoRelayStatus::Ok { - if !out.is_null() { - unsafe { native_string_free(out) }; + let user_data = user_data.clone(); + Box::pin(async move { + clear_native_last_error(); + let request_json = serde_json::to_value(request).map_err(|err| { + FlowError::Internal(format!("failed to serialize LLM request: {err}")) + })?; + let request_string = native_string_from_json(&request_json).ok_or_else(|| { + FlowError::Internal("failed to allocate native LLM request".into()) + })?; + let mut out = ptr::null_mut(); + let status = unsafe { cb(user_data.ptr, request_string, &mut out) }; + unsafe { native_string_free(request_string) }; + if status != NemoRelayStatus::Ok { + if !out.is_null() { + unsafe { native_string_free(out) }; + } + return Err(flow_error_from_status( + status, + "native LLM conditional failed", + )); } - return Err(flow_error_from_status( - status, - "native LLM conditional failed", - )); - } - if out.is_null() { - Ok(None) - } else { - let reason = take_native_string(out)?; - Ok(Some(reason)) - } + if out.is_null() { + Ok(None) + } else { + let reason = take_native_string(out)?; + Ok(Some(reason)) + } + }) }) } @@ -2153,58 +2876,62 @@ fn wrap_llm_request_intercept_fn( ) -> LlmRequestInterceptFn { let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |name, request, annotated| { - clear_native_last_error(); - let name_string = native_string_from_str(name) - .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?; - let request_json = serde_json::to_value(&request).map_err(|err| { - FlowError::Internal(format!("failed to serialize LLM request: {err}")) - })?; - let request_string = native_string_from_json(&request_json) - .ok_or_else(|| FlowError::Internal("failed to allocate native LLM request".into()))?; - let annotated_string = match &annotated { - Some(annotated) => { - let value = serde_json::to_value(annotated).map_err(|err| { - FlowError::Internal(format!("failed to serialize annotated request: {err}")) - })?; - native_string_from_json(&value).ok_or_else(|| { - FlowError::Internal("failed to allocate annotated request".into()) - })? + let user_data = user_data.clone(); + Box::pin(async move { + clear_native_last_error(); + let name_string = native_string_from_str(&name) + .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?; + let request_json = serde_json::to_value(&request).map_err(|err| { + FlowError::Internal(format!("failed to serialize LLM request: {err}")) + })?; + let request_string = native_string_from_json(&request_json).ok_or_else(|| { + FlowError::Internal("failed to allocate native LLM request".into()) + })?; + let annotated_string = match &annotated { + Some(annotated) => { + let value = serde_json::to_value(annotated).map_err(|err| { + FlowError::Internal(format!("failed to serialize annotated request: {err}")) + })?; + native_string_from_json(&value).ok_or_else(|| { + FlowError::Internal("failed to allocate annotated request".into()) + })? + } + None => ptr::null_mut(), + }; + let mut out_outcome = ptr::null_mut(); + let status = unsafe { + cb( + user_data.ptr, + name_string, + request_string, + annotated_string, + &mut out_outcome, + ) + }; + unsafe { + native_string_free(name_string); + native_string_free(request_string); + native_string_free(annotated_string); } - None => ptr::null_mut(), - }; - let mut out_outcome = ptr::null_mut(); - let status = unsafe { - cb( - user_data.ptr, - name_string, - request_string, - annotated_string, - &mut out_outcome, - ) - }; - unsafe { - native_string_free(name_string); - native_string_free(request_string); - native_string_free(annotated_string); - } - if status != NemoRelayStatus::Ok { + if status != NemoRelayStatus::Ok { + unsafe { + native_string_free(out_outcome); + } + return Err(flow_error_from_status( + status, + "native LLM request intercept failed", + )); + } + let outcome_json = json_from_native_string( + out_outcome, + "native LLM request intercept returned null outcome", + ); unsafe { native_string_free(out_outcome); } - return Err(flow_error_from_status( - status, - "native LLM request intercept failed", - )); - } - let outcome_json = json_from_native_string( - out_outcome, - "native LLM request intercept returned null outcome", - ); - unsafe { - native_string_free(out_outcome); - } - serde_json::from_value::(outcome_json?).map_err(|err| { - FlowError::Internal(format!("invalid LLM request intercept outcome JSON: {err}")) + serde_json::from_value::(outcome_json?).map_err(|err| { + FlowError::Internal(format!("invalid LLM request intercept outcome JSON: {err}")) + }) }) }) } diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 79e2e70ce..5262dd784 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -59,9 +59,9 @@ use tower::service_fn; use crate::api::event::{Event, EventSanitizeFields}; use crate::api::llm::{LLM_REQUEST_INTERCEPT_OUTCOME_SCHEMA, LlmRequest}; use crate::api::runtime::{ - LlmCodecIdentity, LlmExecutionNextFn, LlmJsonStream, LlmSanitizeRequestContext, - LlmSanitizeResponseContext, LlmStreamExecutionNextFn, ToolExecutionNextFn, current_scope_stack, - with_scope_stack, + EventSanitizeFn, LlmCodecIdentity, LlmExecutionNextFn, LlmJsonStream, + LlmSanitizeRequestContext, LlmSanitizeResponseContext, LlmStreamExecutionNextFn, + ToolExecutionNextFn, current_scope_stack, with_scope_stack, }; use crate::api::scope::{ EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeHandle, ScopeType, @@ -1119,14 +1119,16 @@ impl WorkerPluginInstance { ) -> crate::plugin::Result<()> { let instance = Arc::new(self.clone_for_callback()); let callback_name = name.to_owned(); - let callback = Arc::new(move |event: &Event, _fields: EventSanitizeFields| { - instance - .invoke_event_sanitize(&callback_name, surface, event) - .unwrap_or_else(|_| { - instance.log_callback_fallback(&callback_name, surface); - EventSanitizeFields::default() + let callback: EventSanitizeFn = + Arc::new(move |event: Event, _fields: EventSanitizeFields| { + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_event_sanitize(&callback_name, surface, &event) + .await }) - }); + }); match surface { RegistrationSurface::MarkSanitizeGuardrail => { ctx.register_mark_sanitize_guardrail(name, priority, callback) @@ -1157,18 +1159,13 @@ impl WorkerPluginInstance { name, priority, Arc::new(move |tool_name, value| { - instance - .invoke_tool_json( - &callback_name, - surface, - tool_name, - value.clone(), - None, - ) - .unwrap_or_else(|_| { - instance.log_callback_fallback(&callback_name, surface); - value - }) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_tool_json(&callback_name, surface, &tool_name, value, None) + .await + }) }), ), RegistrationSurface::ToolSanitizeResponseGuardrail => ctx @@ -1176,18 +1173,13 @@ impl WorkerPluginInstance { name, priority, Arc::new(move |tool_name, value| { - instance - .invoke_tool_json( - &callback_name, - surface, - tool_name, - value.clone(), - None, - ) - .unwrap_or_else(|_| { - instance.log_callback_fallback(&callback_name, surface); - value - }) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_tool_json(&callback_name, surface, &tool_name, value, None) + .await + }) }), ), RegistrationSurface::ToolConditionalExecutionGuardrail => ctx @@ -1195,7 +1187,13 @@ impl WorkerPluginInstance { name, priority, Arc::new(move |tool_name, value| { - instance.invoke_tool_guardrail(&callback_name, tool_name, value.clone()) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_tool_guardrail(&callback_name, &tool_name, value) + .await + }) }), ), RegistrationSurface::ToolRequestIntercept => ctx.register_tool_request_intercept( @@ -1203,7 +1201,13 @@ impl WorkerPluginInstance { priority, registration.break_chain, Arc::new(move |tool_name, value| { - instance.invoke_tool_json(&callback_name, surface, tool_name, value, None) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_tool_json(&callback_name, surface, &tool_name, value, None) + .await + }) }), ), RegistrationSurface::ToolExecutionIntercept => ctx.register_tool_execution_intercept( @@ -1244,12 +1248,13 @@ impl WorkerPluginInstance { name, priority, Arc::new(move |request, context| { - instance - .invoke_llm_sanitize_request(&callback_name, request.clone(), context) - .unwrap_or_else(|_| { - instance.log_callback_fallback(&callback_name, surface); - None - }) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_llm_sanitize_request(&callback_name, request, context) + .await + }) }), ), RegistrationSurface::LlmSanitizeResponseGuardrail => ctx @@ -1257,12 +1262,13 @@ impl WorkerPluginInstance { name, priority, Arc::new(move |value, context| { - instance - .invoke_llm_sanitize_response(&callback_name, value.clone(), context) - .unwrap_or_else(|_| { - instance.log_callback_fallback(&callback_name, surface); - None - }) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_llm_sanitize_response(&callback_name, value, context) + .await + }) }), ), RegistrationSurface::LlmConditionalExecutionGuardrail => ctx @@ -1270,7 +1276,11 @@ impl WorkerPluginInstance { name, priority, Arc::new(move |request| { - instance.invoke_llm_guardrail(&callback_name, request.clone()) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance.invoke_llm_guardrail(&callback_name, request).await + }) }), ), RegistrationSurface::LlmRequestIntercept => ctx.register_llm_request_intercept( @@ -1278,12 +1288,18 @@ impl WorkerPluginInstance { priority, registration.break_chain, Arc::new(move |model_name, request, annotated| { - instance.invoke_llm_request_intercept( - &callback_name, - model_name, - request, - annotated, - ) + let instance = instance.clone(); + let callback_name = callback_name.clone(); + Box::pin(async move { + instance + .invoke_llm_request_intercept( + &callback_name, + &model_name, + request, + annotated, + ) + .await + }) }), ), RegistrationSurface::LlmExecutionIntercept => ctx.register_llm_execution_intercept( @@ -1476,7 +1492,7 @@ impl WorkerPluginCallback { } } - fn invoke_event_sanitize( + async fn invoke_event_sanitize( &self, registration_name: &str, surface: RegistrationSurface, @@ -1488,7 +1504,7 @@ impl WorkerPluginCallback { None, Some(invoke_request_payload_event(event)), ); - let value = json_from_invoke_response(self.invoke_blocking(request)?)?; + let value = json_from_invoke_response(self.invoke_async(request).await?)?; serde_json::from_value(value).map_err(|err| { FlowError::Internal(format!( "worker returned invalid event sanitize fields: {err}" @@ -1496,7 +1512,7 @@ impl WorkerPluginCallback { }) } - fn invoke_tool_json( + async fn invoke_tool_json( &self, registration_name: &str, surface: RegistrationSurface, @@ -1510,10 +1526,10 @@ impl WorkerPluginCallback { continuation_id, Some(invoke_request_payload_tool(tool_name, value)), ); - json_from_invoke_response(self.invoke_blocking(request)?) + json_from_invoke_response(self.invoke_async(request).await?) } - fn invoke_tool_guardrail( + async fn invoke_tool_guardrail( &self, registration_name: &str, tool_name: &str, @@ -1525,7 +1541,7 @@ impl WorkerPluginCallback { None, Some(invoke_request_payload_tool(tool_name, value)), ); - guardrail_from_invoke_response(self.invoke_blocking(request)?) + guardrail_from_invoke_response(self.invoke_async(request).await?) } async fn invoke_tool_execution( @@ -1568,7 +1584,7 @@ impl WorkerPluginCallback { } } - fn invoke_llm_sanitize_request( + async fn invoke_llm_sanitize_request( &self, registration_name: &str, request: LlmRequest, @@ -1606,7 +1622,7 @@ impl WorkerPluginCallback { context.codec_capability_id = Some(capability_id.clone()); capability_id }); - let response = self.invoke_blocking(invoke); + let response = self.invoke_async(invoke).await; if let Some(capability_id) = capability_id { self.host_state.remove_codec(&capability_id); } @@ -1618,7 +1634,7 @@ impl WorkerPluginCallback { }) } - fn invoke_llm_sanitize_response( + async fn invoke_llm_sanitize_response( &self, registration_name: &str, response: Json, @@ -1656,14 +1672,14 @@ impl WorkerPluginCallback { context.codec_capability_id = Some(capability_id.clone()); capability_id }); - let response = self.invoke_blocking(invoke); + let response = self.invoke_async(invoke).await; if let Some(capability_id) = capability_id { self.host_state.remove_codec(&capability_id); } optional_json_from_invoke_response(response?) } - fn invoke_llm_guardrail( + async fn invoke_llm_guardrail( &self, registration_name: &str, request: LlmRequest, @@ -1674,10 +1690,10 @@ impl WorkerPluginCallback { None, Some(invoke_request_payload_llm("", Some(request), None, None)), ); - guardrail_from_invoke_response(self.invoke_blocking(invoke)?) + guardrail_from_invoke_response(self.invoke_async(invoke).await?) } - fn invoke_llm_request_intercept( + async fn invoke_llm_request_intercept( &self, registration_name: &str, model_name: &str, @@ -1695,7 +1711,7 @@ impl WorkerPluginCallback { None, )), ); - let response = self.invoke_blocking(invoke)?; + let response = self.invoke_async(invoke).await?; match response.result { Some(invoke_response_result::Result::LlmRequest(result)) => { let outcome = required_envelope(result.outcome, "llm request intercept outcome")?; @@ -1853,8 +1869,22 @@ impl WorkerPluginCallback { } async fn invoke_async(&self, request: InvokeRequest) -> FlowResult { - self.invoke_async_with_timeout(request, WORKER_RPC_TIMEOUT) - .await + let callback_name = request.registration_name.clone(); + let surface = request.surface; + let result = self + .invoke_async_with_timeout(request, WORKER_RPC_TIMEOUT) + .await; + if let Err(error) = &result { + log::warn!( + target: "nemo_relay.worker", + event = "worker_callback_failed", + plugin_id = self.plugin_kind.as_str(), + callback = callback_name.as_str(), + surface; + "Worker plugin callback failed: {error}" + ); + } + result } async fn invoke_async_with_timeout( diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index 3fee8dd3e..756f066a7 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -39,14 +39,16 @@ use crate::api::optimization::finalize_optimization_summary; use crate::api::runtime::LlmSanitizeResponseContext; use crate::api::runtime::NemoRelayContextState; use crate::api::runtime::global_context; +use crate::api::runtime::subscriber_dispatcher; use crate::api::runtime::{ EventSubscriberFn, LlmJsonStream, LlmStreamInner, ScopeStackHandle, current_scope_stack, }; -use crate::api::shared::metadata_with_otel_status; -use crate::api::shared::sanitize_event_with_scope_stack; +use crate::api::shared::{ + metadata_with_otel_status, sanitize_event_with_scope_stack, snapshot_event_sanitizers, +}; use crate::codec::response::{AnnotatedLlmResponse, attach_estimated_cost_for_provider}; use crate::codec::traits::LlmResponseCodec; -use crate::error::Result; +use crate::error::{FlowError, Result}; use crate::json::Json; use serde_json::Map; @@ -78,6 +80,8 @@ pub struct LlmStreamWrapper { chunk_index: u64, ended: bool, close_result: Option>, + finalization: Option>, + terminal_result: Option>, } impl LlmStreamWrapper { @@ -157,6 +161,8 @@ impl LlmStreamWrapper { chunk_index: 0, ended: false, close_result: None, + finalization: None, + terminal_result: None, } } @@ -182,7 +188,12 @@ impl LlmStreamWrapper { "ERROR", Some("stream dropped before clean completion".to_string()), ); - self.emit_end_event(metadata, true); + // Drop cannot await the async finalizer. Close the recorder before + // spawning it so late optimization evidence is rejected immediately. + self.handle + .optimization_recorder + .close_for_finalization(Some("stream_interrupted")); + self.finalization = self.emit_end_event(metadata, true, true); } fn finish_with_status( @@ -197,14 +208,23 @@ impl LlmStreamWrapper { self.ended = true; let metadata = metadata_with_otel_status(self.metadata.clone(), status_code, status_message); - self.emit_end_event(metadata, interrupted); + self.finalization = self.emit_end_event(metadata, interrupted, false); } /// Emit the LLM END event with aggregated response data. /// /// Calls the finalizer to produce the aggregated response, runs sanitize /// response guardrails, and emits the END event. - fn emit_end_event(&mut self, metadata: Option, interrupted: bool) { + fn emit_end_event( + &mut self, + metadata: Option, + interrupted: bool, + background_thread: bool, + ) -> Option> { + // The finalizer below runs on the caller's Tokio runtime. Register a + // dispatcher barrier before spawning it so a synchronous subscriber + // flush after this stream is dropped cannot overtake the END event. + let publication_barrier = subscriber_dispatcher::register_async_publication(); let aggregated = match self.finalizer.take() { Some(finalizer) => finalizer(), None => Json::Null, @@ -230,68 +250,108 @@ impl LlmStreamWrapper { Err(_) => None, } }; - let Some(entries) = snapshot else { - return; - }; - let sanitized = NemoRelayContextState::llm_sanitize_response_snapshot_chain( - response, - self.sanitize_context.clone(), - &entries, - ); - let data = match sanitized { - Some(response) if response_was_null_without_fallback && response.is_null() => None, - response => response, - }; - let annotation_omitted = data.as_ref().is_none_or(Json::is_null); - let mut annotated_response: Option = (!annotation_omitted) - .then(|| { - data.as_ref().and_then(|response| { - self.response_codec.as_ref().and_then(|codec| { - let mut decoded = codec.decode_response(response).ok()?; - attach_estimated_cost_for_provider(&mut decoded, Some(&self.handle.name)); - Some(decoded) + let entries = snapshot?; + let handle = self.handle.clone(); + let scope_stack = self.scope_stack.clone(); + let subscribers = self.subscribers.clone(); + let response_codec = self.response_codec.clone(); + let sanitize_context = self.sanitize_context.clone(); + let finalize = async move { + let sanitized = NemoRelayContextState::llm_sanitize_response_snapshot_chain( + response, + sanitize_context, + &entries, + ) + .await; + let data = match sanitized { + Some(response) if response_was_null_without_fallback && response.is_null() => None, + response => response, + }; + let annotation_omitted = data.as_ref().is_none_or(Json::is_null); + let mut annotated_response: Option = (!annotation_omitted) + .then(|| { + data.as_ref().and_then(|response| { + response_codec.as_ref().and_then(|codec| { + let mut decoded = codec.decode_response(response).ok()?; + attach_estimated_cost_for_provider(&mut decoded, Some(&handle.name)); + Some(decoded) + }) }) }) - }) - .flatten(); - let interruption = (interrupted - && !has_authoritative_final_usage(annotated_response.as_ref())) - .then_some("stream_interrupted"); - self.handle - .optimization_recorder - .close_for_finalization(interruption); - emit_optimization_marks(&self.handle, &self.subscribers); - let pricing = crate::codec::response::active_pricing_resolver(); - let summary = finalize_optimization_summary( - &self.handle.optimization_recorder, - annotated_response.as_mut(), - self.handle.model_name.as_deref(), - &pricing, - ); - if !annotation_omitted - && annotated_response.is_none() - && let Some(summary) = summary - { - annotated_response = Some(AnnotatedLlmResponse { - optimization_summary: Some(summary), - ..AnnotatedLlmResponse::default() + .flatten(); + let interruption = (interrupted + && !has_authoritative_final_usage(annotated_response.as_ref())) + .then_some("stream_interrupted"); + handle + .optimization_recorder + .close_for_finalization(interruption); + emit_optimization_marks(&handle, &subscribers).await; + let pricing = crate::codec::response::active_pricing_resolver(); + let summary = finalize_optimization_summary( + &handle.optimization_recorder, + annotated_response.as_mut(), + handle.model_name.as_deref(), + &pricing, + ); + if !annotation_omitted + && annotated_response.is_none() + && let Some(summary) = summary + { + annotated_response = Some(AnnotatedLlmResponse { + optimization_summary: Some(summary), + ..AnnotatedLlmResponse::default() + }); + } + let annotated_response = annotated_response.map(Arc::new); + let event_snapshot = { + let ctx = global_context(); + let state = ctx.read(); + match state { + Ok(state) => { + Some(state.end_llm_handle(&handle, data, metadata, annotated_response)) + } + Err(_) => None, + } + }; + if let Some(event) = event_snapshot + && let Some(event) = sanitize_event_with_scope_stack(event, &scope_stack).await + { + let _ = subscriber_dispatcher::dispatch_sanitized_event( + event, + Vec::new(), + &subscribers, + scope_stack.clone(), + ); + } + if let Some(done) = publication_barrier { + let _ = done.send(()); + } + }; + if background_thread { + // `Drop` can run while the current-thread Tokio executor is + // synchronously flushing subscribers. Use a dedicated runtime so + // the FIFO publication barrier can still be released. + std::thread::spawn(move || { + if let Ok(runtime) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + runtime.block_on(finalize); + } }); + return None; } - let annotated_response = annotated_response.map(Arc::new); - let event_snapshot = { - let ctx = global_context(); - let state = ctx.read(); - match state { - Ok(state) => { - Some(state.end_llm_handle(&self.handle, data, metadata, annotated_response)) + match tokio::runtime::Handle::try_current() { + Ok(handle) => Some(handle.spawn(finalize)), + Err(_) => { + if let Ok(runtime) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + runtime.block_on(finalize); } - Err(_) => None, + None } - }; - if let Some(event) = event_snapshot - && let Some(event) = sanitize_event_with_scope_stack(event, &self.scope_stack) - { - NemoRelayContextState::emit_event(&event, &self.subscribers); } } @@ -318,9 +378,14 @@ impl LlmStreamWrapper { } }; if let Some(event) = event_snapshot - && let Some(event) = sanitize_event_with_scope_stack(event, &self.scope_stack) + && let Some(sanitizers) = snapshot_event_sanitizers(&event, &self.scope_stack) { - NemoRelayContextState::emit_event(&event, &self.subscribers); + let _ = subscriber_dispatcher::dispatch_sanitized_event( + event, + sanitizers, + &self.subscribers, + self.scope_stack.clone(), + ); } } } @@ -328,8 +393,31 @@ impl LlmStreamWrapper { impl Stream for LlmStreamWrapper { type Item = Result; - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.as_mut().get_mut(); + + // The END event runs async because response and event sanitizers may + // await. Do not expose stream termination until that work has queued + // the event: callers commonly flush subscribers immediately after + // exhausting a stream, and that flush must include its END event. + if let Some(finalization) = this.finalization.as_mut() { + return match Pin::new(finalization).poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(())) => { + this.finalization = None; + match this.terminal_result.take() { + Some(result) => Poll::Ready(Some(result)), + None => Poll::Ready(None), + } + } + Poll::Ready(Err(error)) => { + this.finalization = None; + Poll::Ready(Some(Err(FlowError::Internal(format!( + "stream finalization task failed: {error}" + ))))) + } + }; + } if this.ended { return Poll::Ready(None); @@ -346,19 +434,21 @@ impl Stream for LlmStreamWrapper { Ok(()) => Poll::Ready(Some(Ok(raw_chunk))), Err(e) => { let message = e.to_string(); + this.terminal_result = Some(Err(e)); this.finish_with_status("ERROR", Some(message), true); - Poll::Ready(Some(Err(e))) + self.poll_next(cx) } } } Poll::Ready(Some(Err(e))) => { let message = e.to_string(); + this.terminal_result = Some(Err(e)); this.finish_with_status("ERROR", Some(message), true); - Poll::Ready(Some(Err(e))) + self.poll_next(cx) } Poll::Ready(None) => { this.finish_with_status("OK", None, false); - Poll::Ready(None) + self.poll_next(cx) } Poll::Pending => Poll::Pending, } @@ -374,6 +464,11 @@ impl LlmStreamInner for LlmStreamWrapper { } let result = this.inner.close().await; this.finish(); + if let Some(finalization) = this.finalization.take() { + finalization.await.map_err(|error| { + FlowError::Internal(format!("stream finalization task failed: {error}")) + })?; + } this.close_result = Some(result.clone()); this.close_result .as_ref() diff --git a/crates/core/tests/coverage/logging_rotation_tests.rs b/crates/core/tests/coverage/logging_rotation_tests.rs new file mode 100644 index 000000000..842fa7927 --- /dev/null +++ b/crates/core/tests/coverage/logging_rotation_tests.rs @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; + +#[test] +fn rotating_writer_rotates_retains_and_reports_closed_file_errors() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("nested").join("relay.log"); + let mut writer = SizeRotatingFileWriter::new(path.clone(), 4, 2).unwrap(); + assert_eq!(writer.write(b"abcd").unwrap(), 4); + writer.flush().unwrap(); + assert_eq!(writer.write(b"e").unwrap(), 1); + writer.flush().unwrap(); + + assert_eq!(std::fs::read(rotated_log_path(&path, 1)).unwrap(), b"abcd"); + assert_eq!(std::fs::read(&path).unwrap(), b"e"); + + writer.file = None; + assert!(writer.write(b"x").is_err()); + assert!(writer.flush().is_err()); +} + +#[test] +fn rotation_helpers_handle_empty_and_missing_files() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("missing.log"); + rotate_files(&path, 2).unwrap(); + assert_eq!( + rotated_log_path(&path, 2), + directory.path().join("missing.2.log") + ); + create_parent_directory(std::path::Path::new("plain.log")).unwrap(); +} diff --git a/crates/core/tests/coverage/logging_sink_tests.rs b/crates/core/tests/coverage/logging_sink_tests.rs index 044ce2b47..88f142655 100644 --- a/crates/core/tests/coverage/logging_sink_tests.rs +++ b/crates/core/tests/coverage/logging_sink_tests.rs @@ -2,10 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use super::{ - DROP_REPORT_INTERVAL_MILLIS, DropNoticeRateLimiter, dropped_record_error_handler, - log_level_filter, now_millis, spdlog_level, stderr_error_handler, + DROP_REPORT_INTERVAL_MILLIS, DropNoticeRateLimiter, build_logger, dropped_record_error_handler, + log_level_filter, now_millis, reserved_sink_paths, resolve_log_path, rotated_log_path, + spdlog_level, stderr_error_handler, }; use crate::logging::LogLevel; +use crate::logging::{ + FileLogRotationConfig, FileLogSinkConfig, LogFormat, LogSinkConfig, LoggingConfig, + MAX_FILE_SINK_QUEUE_ENTRIES, +}; +use std::path::PathBuf; #[test] fn drop_notice_rate_limiter_reports_immediately_then_once_per_interval() { @@ -33,3 +39,60 @@ fn sink_helpers_cover_boundary_levels_time_and_emergency_handlers() { "expected test error", ))); } + +#[test] +fn logger_builder_rejects_duplicate_conflicting_and_invalid_file_sinks() { + let directory = tempfile::tempdir().unwrap(); + let log_path = directory.path().join("relay.log"); + let file_sink = |path: PathBuf, rotation| { + LogSinkConfig::File(FileLogSinkConfig { + path, + level: LogLevel::Info, + format: LogFormat::Jsonl, + queue_capacity: 8, + rotation, + }) + }; + + assert!(resolve_log_path(std::path::Path::new("")).is_err()); + let rotation = FileLogRotationConfig::new(32, 1).unwrap(); + assert_eq!(reserved_sink_paths(&log_path, Some(rotation)).len(), 2); + + let duplicate = LoggingConfig { + sinks: vec![ + file_sink(log_path.clone(), None), + file_sink(log_path.clone(), None), + ], + ..LoggingConfig::default() + }; + let error = match build_logger(&duplicate, "root".into()) { + Ok(_) => panic!("duplicate file sinks must be rejected"), + Err(error) => error, + }; + assert!(error.to_string().contains("duplicate logging sink path")); + + let conflict = LoggingConfig { + sinks: vec![ + file_sink(log_path.clone(), Some(rotation)), + file_sink(rotated_log_path(&log_path, 1), None), + ], + ..LoggingConfig::default() + }; + let error = match build_logger(&conflict, "root".into()) { + Ok(_) => panic!("active and rotated file paths must not overlap"), + Err(error) => error, + }; + assert!(error.to_string().contains("conflicts")); + + let mut invalid_capacity = LoggingConfig { + sinks: vec![file_sink(log_path, None)], + ..LoggingConfig::default() + }; + let LogSinkConfig::File(file_sink) = &mut invalid_capacity.sinks[0]; + file_sink.queue_capacity = MAX_FILE_SINK_QUEUE_ENTRIES + 1; + let error = match build_logger(&invalid_capacity, "root".into()) { + Ok(_) => panic!("oversized async queues must be rejected"), + Err(error) => error, + }; + assert!(error.to_string().contains("queue_capacity")); +} diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index 350cf1414..acade0d0c 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -7,7 +7,9 @@ use std::ptr; use nemo_relay_plugin::{ CategoryProfile, ConfigDiagnostic, DiagnosticLevel, Event, EventCategory, EventSanitizeFields, Json, LlmJsonStream, LlmRequest, LlmRequestInterceptOutcome, NemoRelayNativeHostApiV1, - NemoRelayNativePluginContext, NemoRelayNativePluginV1, NemoRelayNativeString, NemoRelayStatus, + NemoRelayNativeHostApiV3, NemoRelayNativeAsyncCallbackState, NemoRelayNativeAsyncMiddlewareCb, + NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativePluginContext, NemoRelayNativePluginV1, + NemoRelayNativeString, NemoRelayStatus, NemoRelayNativeToolNextFn, NativePlugin, PendingMarkSpec, PluginContext, PluginRuntime, ScopeCategory, ScopeType, ToolExecutionInterceptOutcome, }; @@ -280,6 +282,33 @@ fn mark_json(mut value: Json, key: &str) -> Json { nemo_relay_plugin::nemo_relay_plugin!(nemo_relay_fixture_native_plugin, || FixtureNativePlugin); +#[unsafe(no_mangle)] +pub unsafe extern "C" fn nemo_relay_fixture_async_entry( + host: *const NemoRelayNativeHostApiV1, + out: *mut NemoRelayNativePluginV1, +) -> NemoRelayStatus { + if host.is_null() || out.is_null() { + return NemoRelayStatus::NullPointer; + } + let host_v1 = unsafe { &*host }; + if host_v1.abi_version < 3 + || host_v1.struct_size < std::mem::size_of::() + { + return NemoRelayStatus::InvalidArg; + } + let host_v2 = unsafe { &*(host as *const NemoRelayNativeHostApiV3) }; + let mut plugin = NemoRelayNativePluginV1::default(); + plugin.plugin_kind = unsafe { raw_host_string(&host_v2.v1, "fixture_async") }; + if plugin.plugin_kind.is_null() { + return NemoRelayStatus::Internal; + } + plugin.user_data = Box::into_raw(Box::new(*host_v2)).cast(); + plugin.register = Some(raw_register_async_tool_request); + plugin.drop = Some(raw_drop_async_host); + unsafe { *out = plugin }; + NemoRelayStatus::Ok +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn nemo_relay_fixture_observability_collision( host: *const NemoRelayNativeHostApiV1, @@ -570,6 +599,260 @@ unsafe extern "C" fn raw_register_event_sanitize_errors( status } +unsafe extern "C" fn raw_register_async_tool_request( + user_data: *mut c_void, + _plugin_config_json: *const NemoRelayNativeString, + ctx: *mut NemoRelayNativePluginContext, +) -> NemoRelayStatus { + if user_data.is_null() { + return NemoRelayStatus::NullPointer; + } + let host = unsafe { &*(user_data as *const NemoRelayNativeHostApiV3) }; + let registrations: [ + (NemoRelayNativeAsyncMiddlewareKind, &str, NemoRelayNativeAsyncMiddlewareCb); + 14 + ] = [ + (NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest, "fixture_async_tool_sanitize_request", raw_async_passthrough_callback), + (NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse, "fixture_async_tool_sanitize_response", raw_async_passthrough_callback), + (NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution, "fixture_async_tool_conditional", raw_async_allow_callback), + (NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept, "fixture_async_request", raw_async_tool_request_callback), + (NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept, "fixture_async_execution", raw_async_tool_execution_callback), + (NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest, "fixture_async_llm_sanitize_request", raw_async_passthrough_callback), + (NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse, "fixture_async_llm_sanitize_response", raw_async_passthrough_callback), + (NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution, "fixture_async_llm_conditional", raw_async_allow_callback), + (NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept, "fixture_async_llm_request", raw_async_passthrough_callback), + (NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept, "fixture_async_llm_execution", raw_async_tool_execution_callback), + (NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept, "fixture_async_llm_stream", raw_async_tool_execution_callback), + (NemoRelayNativeAsyncMiddlewareKind::MarkSanitize, "fixture_async_mark", raw_async_passthrough_callback), + (NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart, "fixture_async_scope_start", raw_async_passthrough_callback), + (NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd, "fixture_async_scope_end", raw_async_passthrough_callback), + ]; + for (kind, registration_name, callback) in registrations { + let name = unsafe { raw_host_string(&host.v1, registration_name) }; + if name.is_null() { + return NemoRelayStatus::Internal; + } + let status = unsafe { + (host.plugin_context_register_async_middleware)( + ctx, kind, name, 0, false, callback, user_data, None, + ) + }; + unsafe { (host.v1.string_free)(name) }; + if status != NemoRelayStatus::Ok { + return status; + } + } + NemoRelayStatus::Ok +} + +unsafe extern "C" fn raw_async_allow_callback( + user_data: *mut c_void, + _invocation_json: *const NemoRelayNativeString, + _next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, + completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, +) -> NemoRelayNativeAsyncCallbackState { + let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { + return NemoRelayNativeAsyncCallbackState::Complete; + }; + let result = unsafe { raw_host_string(&host.v1, "null") }; + if result.is_null() { + unsafe { reject_async_completion(host, completion, "failed to allocate async allow result") }; + } else { + unsafe { + (host.async_completion_resolve_json)(completion, result); + (host.v1.string_free)(result); + } + } + NemoRelayNativeAsyncCallbackState::Complete +} + +unsafe extern "C" fn raw_async_passthrough_callback( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + _next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, + completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, +) -> NemoRelayNativeAsyncCallbackState { + let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { + return NemoRelayNativeAsyncCallbackState::Complete; + }; + let result = unsafe { raw_host_string_value(&host.v1, invocation_json) } + .and_then(|value| serde_json::from_str::(&value).ok()) + .and_then(|invocation| { + invocation.get("annotated").map(|annotated| { + json!({ + "request": invocation["request"], + "annotated_request": annotated, + "pending_marks": [], + "optimization_contributions": [], + }) + }).or_else(|| { + ["value", "request", "response", "fields"] + .into_iter() + .find_map(|key| invocation.get(key).cloned()) + }) + }) + .and_then(|value| serde_json::to_string(&value).ok()); + let Some(result) = result else { + unsafe { reject_async_completion(host, completion, "invalid async passthrough invocation") }; + return NemoRelayNativeAsyncCallbackState::Complete; + }; + let result = unsafe { raw_host_string(&host.v1, &result) }; + if result.is_null() { + unsafe { reject_async_completion(host, completion, "failed to allocate async passthrough result") }; + } else { + unsafe { + (host.async_completion_resolve_json)(completion, result); + (host.v1.string_free)(result); + } + } + NemoRelayNativeAsyncCallbackState::Complete +} + +unsafe extern "C" fn raw_async_tool_request_callback( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + _next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, + completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, +) -> NemoRelayNativeAsyncCallbackState { + let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { + return NemoRelayNativeAsyncCallbackState::Complete; + }; + let invocation = unsafe { raw_host_string_value(&host.v1, invocation_json) } + .and_then(|json| serde_json::from_str::(&json).ok()) + .and_then(|mut invocation| { + let pending = invocation["name"].as_str() == Some("async-pending"); + let duplicate = invocation["name"].as_str() == Some("async-double"); + invocation + .get_mut("value") + .and_then(Json::as_object_mut) + .map(|value| { + value.insert("native_async".into(), json!(true)); + (Json::Object(value.clone()), pending, duplicate) + }) + }) + .and_then(|(value, pending, duplicate)| { + serde_json::to_string(&value) + .ok() + .map(|value| (value, pending, duplicate)) + }); + let Some((result, pending, duplicate)) = invocation else { + unsafe { reject_async_completion(host, completion, "invalid async tool request invocation") }; + return NemoRelayNativeAsyncCallbackState::Complete; + }; + if pending { + let host = *host; + let completion = completion as usize; + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(10)); + let result = unsafe { raw_host_string(&host.v1, &result) }; + if !result.is_null() { + unsafe { + (host.async_completion_resolve_json)( + completion as *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, + result, + ); + (host.v1.string_free)(result); + (host.async_completion_release)( + completion as *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, + ); + } + } else { + unsafe { + reject_async_completion( + &host, + completion as *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, + "failed to allocate async tool request result", + ); + (host.async_completion_release)( + completion as *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, + ); + } + } + }); + return NemoRelayNativeAsyncCallbackState::Pending; + } + let result = unsafe { raw_host_string(&host.v1, &result) }; + if !result.is_null() { + unsafe { + (host.async_completion_resolve_json)(completion, result); + if duplicate { + let _ = (host.async_completion_resolve_json)(completion, result); + } + (host.v1.string_free)(result); + } + } else { + unsafe { reject_async_completion(host, completion, "failed to allocate async tool request result") }; + } + NemoRelayNativeAsyncCallbackState::Complete +} + +unsafe extern "C" fn raw_async_tool_execution_callback( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, + completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, +) -> NemoRelayNativeAsyncCallbackState { + let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { + return NemoRelayNativeAsyncCallbackState::Complete; + }; + if next.is_null() || completion.is_null() { + unsafe { reject_async_completion(host, completion, "async tool execution requires next and completion") }; + return NemoRelayNativeAsyncCallbackState::Complete; + } + let value = unsafe { raw_host_string_value(&host.v1, invocation_json) } + .and_then(|json| serde_json::from_str::(&json).ok()) + .and_then(|mut invocation| { + if let Some(value) = invocation.get_mut("value").and_then(Json::as_object_mut) { + value.insert("native_async_execution".into(), json!(true)); + Some(Json::Object(value.clone())) + } else { + invocation.get("request").cloned() + } + }) + .and_then(|value| serde_json::to_string(&value).ok()); + let Some(value) = value else { + unsafe { reject_async_completion(host, completion, "invalid async tool execution invocation") }; + return NemoRelayNativeAsyncCallbackState::Complete; + }; + let value = unsafe { raw_host_string(&host.v1, &value) }; + if value.is_null() { + unsafe { reject_async_completion(host, completion, "failed to allocate async tool execution invocation") }; + return NemoRelayNativeAsyncCallbackState::Complete; + } + let status = unsafe { (host.async_next_invoke)(next, value, completion) }; + unsafe { + (host.v1.string_free)(value); + } + if status == NemoRelayStatus::Ok { + unsafe { + (host.async_next_release)(next); + (host.async_completion_release)(completion); + } + NemoRelayNativeAsyncCallbackState::Pending + } else { + unsafe { reject_async_completion(host, completion, "failed to invoke async tool execution next") }; + NemoRelayNativeAsyncCallbackState::Complete + } +} + +unsafe fn reject_async_completion( + host: &NemoRelayNativeHostApiV3, + completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, + message: &str, +) { + if completion.is_null() { + return; + } + let message = unsafe { raw_host_string(&host.v1, message) }; + if message.is_null() { + return; + } + unsafe { + let _ = (host.async_completion_reject)(completion, message); + (host.v1.string_free)(message); + } +} + unsafe extern "C" fn raw_tool_outcome_callback( user_data: *mut c_void, name: *const NemoRelayNativeString, @@ -638,6 +921,12 @@ unsafe extern "C" fn raw_drop_host(user_data: *mut c_void) { } } +unsafe extern "C" fn raw_drop_async_host(user_data: *mut c_void) { + if !user_data.is_null() { + drop(unsafe { Box::from_raw(user_data as *mut NemoRelayNativeHostApiV3) }); + } +} + unsafe fn raw_host_from_user_data<'a>( user_data: *mut c_void, ) -> Option<&'a NemoRelayNativeHostApiV1> { diff --git a/crates/core/tests/integration/api_surface_tests.rs b/crates/core/tests/integration/api_surface_tests.rs index 7a6045ac0..61f56251f 100644 --- a/crates/core/tests/integration/api_surface_tests.rs +++ b/crates/core/tests/integration/api_surface_tests.rs @@ -7,6 +7,9 @@ use std::sync::{Arc, Mutex}; +mod test_support; +use test_support::ready; + use chrono::{DateTime, TimeDelta, Utc}; use futures::StreamExt; use nemo_relay::api::event::{CategoryProfile, Event, ScopeCategory}; @@ -65,6 +68,7 @@ use nemo_relay::api::tool::{ tool_call, tool_call_end, tool_call_execute, tool_conditional_execution, tool_request_intercepts, }; +use nemo_relay::codec::optimization::LlmOptimizationContribution; use nemo_relay::error::{FlowError, Result}; use nemo_relay::json::Json; use serde_json::{Map, json}; @@ -90,7 +94,7 @@ fn event_sanitizers_rewrite_only_observability_fields_in_priority_order() { Arc::new(|event, mut fields| { assert_eq!(event.data().unwrap()["order"], json!(["early"])); fields.data = Some(json!({"order": ["early", "late"]})); - fields + ready(fields) }), ) .unwrap(); @@ -101,7 +105,7 @@ fn event_sanitizers_rewrite_only_observability_fields_in_priority_order() { fields.data = Some(json!({"order": ["early"]})); fields.metadata = Some(json!({"redacted": true})); fields.category_profile = Some(CategoryProfile::builder().subtype("sanitized").build()); - fields + ready(fields) }), ) .unwrap(); @@ -151,7 +155,7 @@ fn mark_and_scope_local_sanitizers_cover_marks_and_tool_scopes() { 20, Arc::new(|_, mut fields| { fields.data = Some(json!({"mark": "global"})); - fields + ready(fields) }), ) .unwrap(); @@ -160,7 +164,7 @@ fn mark_and_scope_local_sanitizers_cover_marks_and_tool_scopes() { 20, Arc::new(|_, mut fields| { fields.metadata = Some(json!({"scope_end": true})); - fields + ready(fields) }), ) .unwrap(); @@ -178,7 +182,7 @@ fn mark_and_scope_local_sanitizers_cover_marks_and_tool_scopes() { 10, Arc::new(|_, mut fields| { fields.data = Some(json!({"mark": "local"})); - fields + ready(fields) }), ) .unwrap(); @@ -188,7 +192,7 @@ fn mark_and_scope_local_sanitizers_cover_marks_and_tool_scopes() { 10, Arc::new(|_, mut fields| { fields.metadata = Some(json!({"scope_start": true})); - fields + ready(fields) }), ) .unwrap(); @@ -198,7 +202,7 @@ fn mark_and_scope_local_sanitizers_cover_marks_and_tool_scopes() { 10, Arc::new(|_, mut fields| { fields.data = Some(json!({"scope_end": "local"})); - fields + ready(fields) }), ) .unwrap(); @@ -512,7 +516,7 @@ fn skill_load_detection_uses_original_arguments_before_observability_sanitizatio register_tool_sanitize_request_guardrail( "strip-skill-path", 1, - Arc::new(|_name, _args| json!({"path": "[redacted]"})), + Arc::new(|_name, _args| ready(json!({"path": "[redacted]"}))), ) .unwrap(); let events = capture_events("sanitized-skill-load-api-events"); @@ -575,7 +579,7 @@ async fn managed_skill_load_marks_survive_failures_repeat_per_call_and_skip_bloc register_tool_conditional_execution_guardrail( "block-skill-load", 1, - Arc::new(|_name, _args| Ok(Some("blocked before start".into()))), + Arc::new(|_name, _args| Box::pin(async { Ok(Some("blocked before start".into())) })), ) .unwrap(); let blocked = tool_call_execute( @@ -703,6 +707,56 @@ fn test_manual_lifecycle_timestamp_overrides() { deregister_subscriber("timestamp-api-events").unwrap(); } +#[test] +fn test_manual_llm_end_queues_optimization_marks_before_end_event() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let events = capture_events("manual-optimization-events"); + let request = make_llm_request(json!({"messages": []})); + let handle = llm_call( + LlmCallParams::builder() + .name("manual-optimized-llm") + .request(&request) + .build(), + ) + .unwrap(); + assert!( + handle + .optimization_recorder + .record(LlmOptimizationContribution::new( + "test.manual", + "test_manual_kind", + )) + ); + + llm_call_end( + nemo_relay::api::llm::LlmCallEndParams::builder() + .handle(&handle) + .response(json!({"ok": true})) + .build(), + ) + .unwrap(); + + let names = captured_events_snapshot(&events) + .into_iter() + .filter(|event| { + event.name() == "manual-optimized-llm" || event.name() == "nemo_relay.llm.optimization" + }) + .map(|event| event.name().to_owned()) + .collect::>(); + assert_eq!( + names, + [ + "manual-optimized-llm", + "nemo_relay.llm.optimization", + "manual-optimized-llm", + ] + ); + deregister_subscriber("manual-optimization-events").unwrap(); +} + #[test] fn test_manual_lifecycle_default_end_timestamps_follow_explicit_starts() { let _lock = TEST_MUTEX.lock().unwrap(); @@ -834,10 +888,19 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { reset_global(); setup_isolated_thread(); - register_mark_sanitize_guardrail("mark-sanitize", 1, Arc::new(|_, fields| fields)).unwrap(); + register_mark_sanitize_guardrail( + "mark-sanitize", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap(); expect_already_exists( - register_mark_sanitize_guardrail("mark-sanitize", 1, Arc::new(|_, fields| fields)) - .unwrap_err(), + register_mark_sanitize_guardrail( + "mark-sanitize", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap_err(), "mark-sanitize", ); assert!(deregister_mark_sanitize_guardrail("mark-sanitize").unwrap()); @@ -846,28 +909,32 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { register_scope_sanitize_start_guardrail( "scope-start-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); assert!(deregister_scope_sanitize_start_guardrail("scope-start-sanitize").unwrap()); assert!(!deregister_scope_sanitize_start_guardrail("scope-start-sanitize").unwrap()); - register_scope_sanitize_end_guardrail("scope-end-sanitize", 1, Arc::new(|_, fields| fields)) - .unwrap(); + register_scope_sanitize_end_guardrail( + "scope-end-sanitize", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap(); assert!(deregister_scope_sanitize_end_guardrail("scope-end-sanitize").unwrap()); assert!(!deregister_scope_sanitize_end_guardrail("scope-end-sanitize").unwrap()); register_tool_sanitize_request_guardrail( "tool-sanitize-request", 1, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap(); expect_already_exists( register_tool_sanitize_request_guardrail( "tool-sanitize-request", 1, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap_err(), "tool-sanitize-request", @@ -878,7 +945,7 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { register_tool_sanitize_response_guardrail( "tool-sanitize-response", 1, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap(); assert!(deregister_tool_sanitize_response_guardrail("tool-sanitize-response").unwrap()); @@ -886,13 +953,18 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { register_tool_conditional_execution_guardrail( "tool-conditional", 1, - Arc::new(|_name, _args| Ok(None)), + Arc::new(|_name, _args| Box::pin(async { Ok(None) })), ) .unwrap(); assert!(deregister_tool_conditional_execution_guardrail("tool-conditional").unwrap()); - register_tool_request_intercept("tool-request", 1, false, Arc::new(|_name, args| Ok(args))) - .unwrap(); + register_tool_request_intercept( + "tool-request", + 1, + false, + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), + ) + .unwrap(); assert!(deregister_tool_request_intercept("tool-request").unwrap()); register_tool_execution_intercept( @@ -906,7 +978,7 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { register_llm_sanitize_request_guardrail( "llm-sanitize-request", 1, - Arc::new(|request, _context| Some(request)), + Arc::new(|request, _context| Box::pin(async move { Ok(Some(request)) })), ) .unwrap(); assert!(deregister_llm_sanitize_request_guardrail("llm-sanitize-request").unwrap()); @@ -914,7 +986,7 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { register_llm_sanitize_response_guardrail( "llm-sanitize-response", 1, - Arc::new(|response, _context| Some(response)), + Arc::new(|response, _context| Box::pin(async move { Ok(Some(response)) })), ) .unwrap(); assert!(deregister_llm_sanitize_response_guardrail("llm-sanitize-response").unwrap()); @@ -922,7 +994,7 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { register_llm_conditional_execution_guardrail( "llm-conditional", 1, - Arc::new(|_request| Ok(None)), + Arc::new(|_request| Box::pin(async { Ok(None) })), ) .unwrap(); assert!(deregister_llm_conditional_execution_guardrail("llm-conditional").unwrap()); @@ -932,7 +1004,7 @@ fn test_global_registry_and_subscriber_wrappers_cover_success_and_duplicates() { 1, false, Arc::new(|_name, request, annotated| { - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( request, annotated, )) }), @@ -1023,7 +1095,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "mark-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); expect_already_exists( @@ -1031,7 +1103,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "mark-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap_err(), "mark-sanitize", @@ -1043,7 +1115,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "scope-start-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); assert!( @@ -1059,7 +1131,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "scope-end-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); assert!( @@ -1073,7 +1145,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "tool-sanitize-request", 1, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap(); expect_already_exists( @@ -1081,7 +1153,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "tool-sanitize-request", 1, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap_err(), "tool-sanitize-request", @@ -1095,7 +1167,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "tool-sanitize-response", 1, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap(); assert!( @@ -1107,7 +1179,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "tool-conditional", 1, - Arc::new(|_name, _args| Ok(None)), + Arc::new(|_name, _args| Box::pin(async { Ok(None) })), ) .unwrap(); assert!( @@ -1120,7 +1192,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss "tool-request", 1, false, - Arc::new(|_name, args| Ok(args)), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap(); assert!(scope_deregister_tool_request_intercept(&scope.uuid, "tool-request").unwrap()); @@ -1138,7 +1210,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "llm-sanitize-request", 1, - Arc::new(|request, _context| Some(request)), + Arc::new(|request, _context| Box::pin(async move { Ok(Some(request)) })), ) .unwrap(); assert!( @@ -1150,7 +1222,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "llm-sanitize-response", 1, - Arc::new(|response, _context| Some(response)), + Arc::new(|response, _context| Box::pin(async move { Ok(Some(response)) })), ) .unwrap(); assert!( @@ -1162,7 +1234,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "llm-conditional", 1, - Arc::new(|_request| Ok(None)), + Arc::new(|_request| Box::pin(async { Ok(None) })), ) .unwrap(); assert!( @@ -1176,7 +1248,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss 1, false, Arc::new(|_name, request, annotated| { - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( request, annotated, )) }), @@ -1229,7 +1301,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "missing-mark-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap_err(), "scope", @@ -1239,7 +1311,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "missing-scope-start-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap_err(), "scope", @@ -1249,7 +1321,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "missing-scope-end-sanitize", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap_err(), "scope", @@ -1259,7 +1331,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss &scope.uuid, "missing-tool-sanitize", 1, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap_err(), "scope", @@ -1270,7 +1342,7 @@ fn test_scope_registry_and_subscriber_wrappers_cover_success_duplicates_and_miss "missing-tool-request", 1, false, - Arc::new(|_name, args| Ok(args)), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap_err(), "scope", @@ -1307,7 +1379,7 @@ async fn test_tool_api_emits_sanitized_events_and_covers_error_paths() { args.as_object_mut() .unwrap() .insert("sanitized_request".into(), json!(true)); - args + ready(args) }), ) .unwrap(); @@ -1319,7 +1391,7 @@ async fn test_tool_api_emits_sanitized_events_and_covers_error_paths() { .as_object_mut() .unwrap() .insert("sanitized_response".into(), json!(true)); - result + ready(result) }), ) .unwrap(); @@ -1331,7 +1403,7 @@ async fn test_tool_api_emits_sanitized_events_and_covers_error_paths() { assert_eq!(event.input().unwrap()["sanitized_request"], true); fields.metadata = Some(json!({"generic_start": true})); } - fields + ready(fields) }), ) .unwrap(); @@ -1343,7 +1415,7 @@ async fn test_tool_api_emits_sanitized_events_and_covers_error_paths() { assert_eq!(event.output().unwrap()["sanitized_response"], true); fields.metadata = Some(json!({"generic_end": true})); } - fields + ready(fields) }), ) .unwrap(); @@ -1401,12 +1473,14 @@ async fn test_tool_api_emits_sanitized_events_and_covers_error_paths() { args.as_object_mut() .unwrap() .insert("intercepted".into(), json!(true)); - Ok(args) + ready(args) }), ) .unwrap(); assert_eq!( - tool_request_intercepts("tool-api", json!({"value": 2})).unwrap()["intercepted"], + tool_request_intercepts("tool-api", json!({"value": 2})) + .await + .unwrap()["intercepted"], json!(true) ); deregister_tool_request_intercept("tool-request").unwrap(); @@ -1414,11 +1488,11 @@ async fn test_tool_api_emits_sanitized_events_and_covers_error_paths() { register_tool_conditional_execution_guardrail( "tool-reject", 1, - Arc::new(|_name, _args| Ok(Some("tool denied".into()))), + Arc::new(|_name, _args| Box::pin(async { Ok(Some("tool denied".into())) })), ) .unwrap(); assert!(matches!( - tool_conditional_execution("tool-api", &json!({"value": 3})), + tool_conditional_execution("tool-api", &json!({"value": 3})).await, Err(FlowError::GuardrailRejected(reason)) if reason == "tool denied" )); assert!(matches!( @@ -1492,7 +1566,7 @@ async fn test_llm_api_emits_sanitized_events_and_covers_error_paths() { 1, Arc::new(|mut request, _context| { request.headers.insert("x-sanitized".into(), json!(true)); - Some(request) + ready(Some(request)) }), ) .unwrap(); @@ -1504,7 +1578,7 @@ async fn test_llm_api_emits_sanitized_events_and_covers_error_paths() { .as_object_mut() .unwrap() .insert("sanitized_response".into(), json!(true)); - Some(response) + ready(Some(response)) }), ) .unwrap(); @@ -1557,7 +1631,7 @@ async fn test_llm_api_emits_sanitized_events_and_covers_error_paths() { false, Arc::new(|_name, mut request, annotated| { request.headers.insert("x-intercepted".into(), json!(true)); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( request, annotated, )) }), @@ -1567,6 +1641,7 @@ async fn test_llm_api_emits_sanitized_events_and_covers_error_paths() { "llm-api", make_llm_request(json!({"messages": [{"role": "user", "content": "hello"}]})), ) + .await .unwrap(); assert_eq!( intercepted.request.headers.get("x-intercepted"), @@ -1577,11 +1652,11 @@ async fn test_llm_api_emits_sanitized_events_and_covers_error_paths() { register_llm_conditional_execution_guardrail( "llm-reject", 1, - Arc::new(|_request| Ok(Some("llm denied".into()))), + Arc::new(|_request| Box::pin(async { Ok(Some("llm denied".into())) })), ) .unwrap(); assert!(matches!( - llm_conditional_execution(&make_llm_request(json!({"messages": []}))), + llm_conditional_execution(&make_llm_request(json!({"messages": []}))).await, Err(FlowError::GuardrailRejected(reason)) if reason == "llm denied" )); assert!(matches!( @@ -1661,7 +1736,7 @@ async fn test_llm_stream_chunk_marks_track_successful_chunks() { Arc::new(|event, mut fields| { assert_eq!(event.name(), "llm.chunk"); fields.metadata = Some(json!({"sanitized": true})); - fields + ready(fields) }), ) .unwrap(); @@ -1703,6 +1778,7 @@ async fn test_llm_stream_chunk_marks_track_successful_chunks() { yielded.push(item.unwrap()); } assert_eq!(yielded, raw_chunks); + stream.close().await.unwrap(); let captured = captured_events_snapshot(&events); assert_eq!(captured.len(), 4); @@ -1775,6 +1851,7 @@ async fn test_llm_stream_chunk_mark_survives_collector_failure() { Err(FlowError::Internal(message)) if message == "collector failed" )); assert!(stream.next().await.is_none()); + stream.close().await.unwrap(); let captured = captured_events_snapshot(&events); assert_eq!(captured.len(), 3); @@ -1841,31 +1918,34 @@ async fn test_llm_stream_api_covers_success_rejection_and_execution_error_paths( chunks, vec![json!({"messages": [{"role": "user", "content": "hello"}]})] ); + stream.close().await.unwrap(); let success_events = captured_events_snapshot(&events); - assert_eq!(success_events[0].kind(), "scope"); - assert_eq!( - success_events[0].scope_category(), - Some(ScopeCategory::Start) - ); - assert_eq!(success_events[0].category().unwrap().as_str(), "llm"); - assert_eq!(success_events.last().unwrap().kind(), "scope"); - assert_eq!( - success_events.last().unwrap().scope_category(), - Some(ScopeCategory::End) - ); - assert_eq!( - success_events.last().unwrap().category().unwrap().as_str(), - "llm" - ); + let success_start = success_events + .iter() + .find(|event| { + event.kind() == "scope" && event.scope_category() == Some(ScopeCategory::Start) + }) + .expect("stream start event"); + let success_end = success_events + .iter() + .rev() + .find(|event| event.kind() == "scope" && event.scope_category() == Some(ScopeCategory::End)) + .expect("stream end event"); + assert_eq!(success_start.kind(), "scope"); + assert_eq!(success_start.scope_category(), Some(ScopeCategory::Start)); + assert_eq!(success_start.category().unwrap().as_str(), "llm"); + assert_eq!(success_end.kind(), "scope"); + assert_eq!(success_end.scope_category(), Some(ScopeCategory::End)); + assert_eq!(success_end.category().unwrap().as_str(), "llm"); assert_eq!( - success_events.last().unwrap().output().unwrap(), + success_end.output().unwrap(), &json!([{"messages": [{"role": "user", "content": "hello"}]}]) ); register_llm_conditional_execution_guardrail( "llm-stream-reject", 1, - Arc::new(|_request| Ok(Some("stream denied".into()))), + Arc::new(|_request| Box::pin(async { Ok(Some("stream denied".into())) })), ) .unwrap(); let reject_collector: Box Result<()> + Send> = Box::new(|_chunk| Ok(())); diff --git a/crates/core/tests/integration/middleware_tests.rs b/crates/core/tests/integration/middleware_tests.rs index c5062b469..d448e7867 100644 --- a/crates/core/tests/integration/middleware_tests.rs +++ b/crates/core/tests/integration/middleware_tests.rs @@ -13,6 +13,9 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; +mod test_support; +use test_support::{ready, ready_result}; + use futures::StreamExt; use nemo_relay::api::event::{ CategoryProfile, DataSchema, Event, EventCategory, PendingMarkSpec, ScopeCategory, @@ -143,8 +146,8 @@ fn assert_middleware_callback_labels( /// Register 3 tool sanitize request guardrails at priorities 1, 3, 2; /// verify execution order is 1, 2, 3. -#[test] -fn test_sanitize_guardrail_priority_ordering() { +#[tokio::test] +async fn test_sanitize_guardrail_priority_ordering() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -158,7 +161,7 @@ fn test_sanitize_guardrail_priority_ordering() { 1, Arc::new(move |_name, args| { o1.lock().unwrap().push(1); - args + ready(args) }), ) .unwrap(); @@ -170,7 +173,7 @@ fn test_sanitize_guardrail_priority_ordering() { 3, Arc::new(move |_name, args| { o3.lock().unwrap().push(3); - args + ready(args) }), ) .unwrap(); @@ -182,7 +185,7 @@ fn test_sanitize_guardrail_priority_ordering() { 2, Arc::new(move |_name, args| { o2.lock().unwrap().push(2); - args + ready(args) }), ) .unwrap(); @@ -195,6 +198,7 @@ fn test_sanitize_guardrail_priority_ordering() { .build(), ) .unwrap(); + flush_subscribers().unwrap(); let recorded = order.lock().unwrap(); assert_eq!( @@ -211,8 +215,8 @@ fn test_sanitize_guardrail_priority_ordering() { /// Register 3 tool request intercepts at priorities 1, 3, 2; /// verify execution order is 1, 2, 3. -#[test] -fn test_request_intercept_priority_ordering() { +#[tokio::test] +async fn test_request_intercept_priority_ordering() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -226,7 +230,7 @@ fn test_request_intercept_priority_ordering() { false, Arc::new(move |_name, args| { o1.lock().unwrap().push(1); - Ok(args) + ready(args) }), ) .unwrap(); @@ -238,7 +242,7 @@ fn test_request_intercept_priority_ordering() { false, Arc::new(move |_name, args| { o3.lock().unwrap().push(3); - Ok(args) + ready(args) }), ) .unwrap(); @@ -250,13 +254,15 @@ fn test_request_intercept_priority_ordering() { false, Arc::new(move |_name, args| { o2.lock().unwrap().push(2); - Ok(args) + ready(args) }), ) .unwrap(); // Use the standalone intercept chain function - let _result = tool_request_intercepts("test_tool", json!({})).unwrap(); + let _result = tool_request_intercepts("test_tool", json!({})) + .await + .unwrap(); let recorded = order.lock().unwrap(); assert_eq!( @@ -272,8 +278,8 @@ fn test_request_intercept_priority_ordering() { } /// Verify that deregistering and re-registering at a different priority re-sorts. -#[test] -fn test_re_registration_at_different_priority_re_sorts() { +#[tokio::test] +async fn test_re_registration_at_different_priority_re_sorts() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -287,7 +293,7 @@ fn test_re_registration_at_different_priority_re_sorts() { false, Arc::new(move |_name, args| { o_a.lock().unwrap().push("a_p10".into()); - Ok(args) + ready(args) }), ) .unwrap(); @@ -299,13 +305,13 @@ fn test_re_registration_at_different_priority_re_sorts() { false, Arc::new(move |_name, args| { o_b.lock().unwrap().push("b_p20".into()); - Ok(args) + ready(args) }), ) .unwrap(); // First call: a runs before b - let _ = tool_request_intercepts("test", json!({})).unwrap(); + let _ = tool_request_intercepts("test", json!({})).await.unwrap(); { let recorded = order.lock().unwrap(); assert_eq!(*recorded, vec!["a_p10", "b_p20"]); @@ -320,14 +326,14 @@ fn test_re_registration_at_different_priority_re_sorts() { false, Arc::new(move |_name, args| { o_a2.lock().unwrap().push("a_p30".into()); - Ok(args) + ready(args) }), ) .unwrap(); // Clear and re-run order.lock().unwrap().clear(); - let _ = tool_request_intercepts("test", json!({})).unwrap(); + let _ = tool_request_intercepts("test", json!({})).await.unwrap(); { let recorded = order.lock().unwrap(); assert_eq!( @@ -348,8 +354,8 @@ fn test_re_registration_at_different_priority_re_sorts() { /// Register 2 request intercepts, first with break_chain=true. /// Verify second intercept is NOT called and the result from the first is used. -#[test] -fn test_break_chain_stops_subsequent_intercepts() { +#[tokio::test] +async fn test_break_chain_stops_subsequent_intercepts() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -364,7 +370,7 @@ fn test_break_chain_stops_subsequent_intercepts() { args.as_object_mut() .unwrap() .insert("breaker_ran".into(), json!(true)); - Ok(args) + ready(args) }), ) .unwrap(); @@ -379,12 +385,12 @@ fn test_break_chain_stops_subsequent_intercepts() { args.as_object_mut() .unwrap() .insert("after_ran".into(), json!(true)); - Ok(args) + ready(args) }), ) .unwrap(); - let result = tool_request_intercepts("tool", json!({})).unwrap(); + let result = tool_request_intercepts("tool", json!({})).await.unwrap(); // First intercept's transformation should be applied assert_eq!(result["breaker_ran"], true); @@ -404,8 +410,8 @@ fn test_break_chain_stops_subsequent_intercepts() { } /// With break_chain=false on all intercepts, both should be called. -#[test] -fn test_no_break_chain_runs_all_intercepts() { +#[tokio::test] +async fn test_no_break_chain_runs_all_intercepts() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -419,7 +425,7 @@ fn test_no_break_chain_runs_all_intercepts() { false, Arc::new(move |_name, args| { c1.fetch_add(1, Ordering::SeqCst); - Ok(args) + ready(args) }), ) .unwrap(); @@ -431,12 +437,12 @@ fn test_no_break_chain_runs_all_intercepts() { false, Arc::new(move |_name, args| { c2.fetch_add(1, Ordering::SeqCst); - Ok(args) + ready(args) }), ) .unwrap(); - let _ = tool_request_intercepts("tool", json!({})).unwrap(); + let _ = tool_request_intercepts("tool", json!({})).await.unwrap(); assert_eq!( call_count.load(Ordering::SeqCst), @@ -690,7 +696,7 @@ async fn test_tool_execution_outcome_marks_follow_end_with_tool_parentage() { let mut metadata = fields.metadata.unwrap_or_else(|| json!({})); metadata["sanitized"] = json!(true); fields.metadata = Some(metadata); - fields + ready(fields) }), ) .unwrap(); @@ -1329,7 +1335,7 @@ async fn test_conditional_guardrail_rejects() { register_tool_conditional_execution_guardrail( "rejector", 1, - Arc::new(|_name, _args| Ok(Some("not allowed".to_string()))), + Arc::new(|_name, _args| Box::pin(async { Ok(Some("not allowed".to_string())) })), ) .unwrap(); @@ -1363,8 +1369,12 @@ async fn test_conditional_guardrail_allows() { reset_global(); setup_isolated_thread(); - register_tool_conditional_execution_guardrail("allower", 1, Arc::new(|_name, _args| Ok(None))) - .unwrap(); + register_tool_conditional_execution_guardrail( + "allower", + 1, + Arc::new(|_name, _args| Box::pin(async { Ok(None) })), + ) + .unwrap(); let func: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); @@ -1402,12 +1412,16 @@ async fn test_tool_conditional_guardrail_emits_guardrail_scope() { ) .unwrap(); - register_tool_conditional_execution_guardrail("tool_scope_allow", 1, Arc::new(|_, _| Ok(None))) - .unwrap(); + register_tool_conditional_execution_guardrail( + "tool_scope_allow", + 1, + Arc::new(|_, _| ready(None)), + ) + .unwrap(); register_tool_conditional_execution_guardrail( "tool_scope_reject", 2, - Arc::new(|_, _| Ok(Some("blocked by tool guardrail".to_string()))), + Arc::new(|_, _| ready(Some("blocked by tool guardrail".to_string()))), ) .unwrap(); @@ -1487,13 +1501,17 @@ async fn test_conditional_guardrail_first_rejection_wins() { reset_global(); setup_isolated_thread(); - register_tool_conditional_execution_guardrail("allows", 1, Arc::new(|_name, _args| Ok(None))) - .unwrap(); + register_tool_conditional_execution_guardrail( + "allows", + 1, + Arc::new(|_name, _args| Box::pin(async { Ok(None) })), + ) + .unwrap(); register_tool_conditional_execution_guardrail( "rejects", 2, - Arc::new(|_name, _args| Ok(Some("blocked by second".to_string()))), + Arc::new(|_name, _args| Box::pin(async { Ok(Some("blocked by second".to_string())) })), ) .unwrap(); @@ -1533,9 +1551,9 @@ async fn test_conditional_guardrail_tool_name_filtering() { 1, Arc::new(|name, _args| { if name == "dangerous_tool" { - Ok(Some("dangerous_tool is forbidden".to_string())) + ready(Some("dangerous_tool is forbidden".to_string())) } else { - Ok(None) + ready(None) } }), ) @@ -1575,8 +1593,8 @@ async fn test_conditional_guardrail_tool_name_filtering() { /// Push scope, register scope-local guardrail, verify it applies, /// pop scope, verify it no longer applies. -#[test] -fn test_scope_local_guardrail_lifecycle() { +#[tokio::test] +async fn test_scope_local_guardrail_lifecycle() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); let handle = setup_isolated_scope("lifecycle_scope"); @@ -1591,7 +1609,7 @@ fn test_scope_local_guardrail_lifecycle() { 1, Arc::new(move |_name, args| { cc.fetch_add(1, Ordering::SeqCst); - args + ready(args) }), ) .unwrap(); @@ -1604,6 +1622,7 @@ fn test_scope_local_guardrail_lifecycle() { .build(), ) .unwrap(); + flush_subscribers().unwrap(); assert_eq!( call_count.load(Ordering::SeqCst), 1, @@ -1700,8 +1719,8 @@ async fn test_scope_local_execution_intercept_cleanup() { /// Register global guardrail at priority 5, scope-local guardrail at priority 3. /// Verify scope-local runs first (lower priority number = higher priority). /// Verify both are applied. -#[test] -fn test_scope_local_and_global_guardrail_merge_priority() { +#[tokio::test] +async fn test_scope_local_and_global_guardrail_merge_priority() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); let handle = setup_isolated_scope("merge_scope"); @@ -1718,7 +1737,7 @@ fn test_scope_local_and_global_guardrail_merge_priority() { args.as_object_mut() .unwrap() .insert("global".into(), json!(true)); - args + ready(args) }), ) .unwrap(); @@ -1734,7 +1753,7 @@ fn test_scope_local_and_global_guardrail_merge_priority() { args.as_object_mut() .unwrap() .insert("local".into(), json!(true)); - args + ready(args) }), ) .unwrap(); @@ -1757,6 +1776,7 @@ fn test_scope_local_and_global_guardrail_merge_priority() { .build(), ) .unwrap(); + flush_subscribers().unwrap(); // Verify order: local (priority 3) runs before global (priority 5) let recorded = order.lock().unwrap(); @@ -1887,7 +1907,7 @@ async fn test_conditional_rejection_prevents_intercepts() { register_tool_conditional_execution_guardrail( "gate", 1, - Arc::new(|_name, _args| Ok(Some("blocked".to_string()))), + Arc::new(|_name, _args| Box::pin(async { Ok(Some("blocked".to_string())) })), ) .unwrap(); @@ -1899,7 +1919,7 @@ async fn test_conditional_rejection_prevents_intercepts() { false, Arc::new(move |_name, args| { ic.store(true, Ordering::SeqCst); - Ok(args) + ready(args) }), ) .unwrap(); @@ -1938,7 +1958,7 @@ async fn test_conditional_rejection_prevents_execution() { register_tool_conditional_execution_guardrail( "gate2", 1, - Arc::new(|_name, _args| Ok(Some("no execution".to_string()))), + Arc::new(|_name, _args| Box::pin(async { Ok(Some("no execution".to_string())) })), ) .unwrap(); @@ -1989,8 +2009,8 @@ async fn test_conditional_rejection_prevents_execution() { // ========================================================================= /// Sanitize guardrails pipe data through sequentially. -#[test] -fn test_sanitize_guardrails_pipe_data() { +#[tokio::test] +async fn test_sanitize_guardrails_pipe_data() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -2003,7 +2023,7 @@ fn test_sanitize_guardrails_pipe_data() { args.as_object_mut() .unwrap() .insert("field_a".into(), json!(true)); - args + ready(args) }), ) .unwrap(); @@ -2018,7 +2038,7 @@ fn test_sanitize_guardrails_pipe_data() { args.as_object_mut() .unwrap() .insert("field_b".into(), json!(has_a)); - args + ready(args) }), ) .unwrap(); @@ -2061,8 +2081,8 @@ fn test_sanitize_guardrails_pipe_data() { } /// Response sanitize guardrails also pipe through. -#[test] -fn test_response_sanitize_guardrails_pipe() { +#[tokio::test] +async fn test_response_sanitize_guardrails_pipe() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -2075,7 +2095,7 @@ fn test_response_sanitize_guardrails_pipe() { .as_object_mut() .unwrap() .insert("sanitized".into(), json!(true)); - result + ready(result) }), ) .unwrap(); @@ -2127,8 +2147,8 @@ fn test_response_sanitize_guardrails_pipe() { /// Use multiple threads to register/deregister guardrails concurrently. /// Verify no panics or data races. -#[test] -fn test_concurrent_register_deregister() { +#[tokio::test] +async fn test_concurrent_register_deregister() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); @@ -2145,7 +2165,7 @@ fn test_concurrent_register_deregister() { let res = register_tool_sanitize_request_guardrail( &name, i, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ); assert!(res.is_ok(), "Registration should succeed for {name}"); @@ -2173,8 +2193,8 @@ fn test_concurrent_register_deregister() { } /// Concurrent register/deregister of intercepts across multiple threads. -#[test] -fn test_concurrent_intercept_mutations() { +#[tokio::test] +async fn test_concurrent_intercept_mutations() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); @@ -2191,7 +2211,7 @@ fn test_concurrent_intercept_mutations() { &name, i, false, - Arc::new(|_name, args| Ok(args)), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ); assert!(res.is_ok()); @@ -2217,8 +2237,8 @@ fn test_concurrent_intercept_mutations() { } /// Interleaved register and tool call execution from multiple threads. -#[test] -fn test_concurrent_register_and_read() { +#[tokio::test] +async fn test_concurrent_register_and_read() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); @@ -2227,7 +2247,7 @@ fn test_concurrent_register_and_read() { register_tool_sanitize_request_guardrail( &format!("stable_{i}"), i, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ) .unwrap(); } @@ -2246,7 +2266,7 @@ fn test_concurrent_register_and_read() { let _ = register_tool_sanitize_request_guardrail( &name, 100 + i, - Arc::new(|_name, args| args), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ); std::thread::yield_now(); let _ = deregister_tool_sanitize_request_guardrail(&name); @@ -2280,8 +2300,8 @@ fn test_concurrent_register_and_read() { // Lock Regression Tests // ========================================================================= -#[test] -fn test_tool_request_intercept_registry_mutations_apply_to_later_calls() { +#[tokio::test] +async fn test_tool_request_intercept_registry_mutations_apply_to_later_calls() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -2308,23 +2328,27 @@ fn test_tool_request_intercept_registry_mutations_apply_to_later_calls() { Arc::new(move |_, args| { record_middleware_callback(&tracked, "tool_request_late"); assert_middleware_callback_locks_are_free(); - Ok(args) + ready(args) }), ) .unwrap(); } - Ok(args) + ready(args) }), ) .unwrap(); - let args = tool_request_intercepts("tool", json!({"round": 1})).unwrap(); + let args = tool_request_intercepts("tool", json!({"round": 1})) + .await + .unwrap(); assert_eq!(args["round"], 1); assert_middleware_callback_labels(&callbacks, &["tool_request_initial"]); callbacks.lock().unwrap().clear(); - let args = tool_request_intercepts("tool", json!({"round": 2})).unwrap(); + let args = tool_request_intercepts("tool", json!({"round": 2})) + .await + .unwrap(); assert_eq!(args["round"], 2); assert_middleware_callback_labels(&callbacks, &["tool_request_initial", "tool_request_late"]); @@ -2332,8 +2356,8 @@ fn test_tool_request_intercept_registry_mutations_apply_to_later_calls() { deregister_tool_request_intercept("snapshot_tool_request_late").unwrap(); } -#[test] -fn test_llm_request_intercept_registry_mutations_apply_to_later_calls() { +#[tokio::test] +async fn test_llm_request_intercept_registry_mutations_apply_to_later_calls() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -2360,7 +2384,7 @@ fn test_llm_request_intercept_registry_mutations_apply_to_later_calls() { Arc::new(move |_, request, annotated| { record_middleware_callback(&tracked, "llm_request_late"); assert_middleware_callback_locks_are_free(); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( request, annotated, )) }), @@ -2368,7 +2392,7 @@ fn test_llm_request_intercept_registry_mutations_apply_to_later_calls() { .unwrap(); } - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( request, annotated, )) }), @@ -2381,6 +2405,7 @@ fn test_llm_request_intercept_registry_mutations_apply_to_later_calls() { content: json!({"round": 1}), }, ) + .await .unwrap(); assert_eq!(request.request.content["round"], 1); assert_middleware_callback_labels(&callbacks, &["llm_request_initial"]); @@ -2393,6 +2418,7 @@ fn test_llm_request_intercept_registry_mutations_apply_to_later_calls() { content: json!({"round": 2}), }, ) + .await .unwrap(); assert_eq!(request.request.content["round"], 2); assert_middleware_callback_labels(&callbacks, &["llm_request_initial", "llm_request_late"]); @@ -2415,7 +2441,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, _| { record_middleware_callback(&tracked, "tool_conditional_global"); assert_middleware_callback_locks_are_free(); - Ok(None) + ready(None) }), ) .unwrap(); @@ -2427,7 +2453,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, _| { record_middleware_callback(&tracked, "tool_conditional_scope"); assert_middleware_callback_locks_are_free(); - Ok(None) + ready(None) }), ) .unwrap(); @@ -2439,7 +2465,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, args| { record_middleware_callback(&tracked, "tool_request_global"); assert_middleware_callback_locks_are_free(); - Ok(args) + ready(args) }), ) .unwrap(); @@ -2452,7 +2478,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, args| { record_middleware_callback(&tracked, "tool_request_scope"); assert_middleware_callback_locks_are_free(); - Ok(args) + ready(args) }), ) .unwrap(); @@ -2463,7 +2489,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, args| { record_middleware_callback(&tracked, "tool_sanitize_request_global"); assert_middleware_callback_locks_are_free(); - args + ready(args) }), ) .unwrap(); @@ -2475,7 +2501,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, args| { record_middleware_callback(&tracked, "tool_sanitize_request_scope"); assert_middleware_callback_locks_are_free(); - args + ready(args) }), ) .unwrap(); @@ -2509,7 +2535,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, result| { record_middleware_callback(&tracked, "tool_sanitize_response_global"); assert_middleware_callback_locks_are_free(); - result + ready(result) }), ) .unwrap(); @@ -2521,7 +2547,7 @@ async fn test_tool_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, result| { record_middleware_callback(&tracked, "tool_sanitize_response_scope"); assert_middleware_callback_locks_are_free(); - result + ready(result) }), ) .unwrap(); @@ -2586,7 +2612,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_| { record_middleware_callback(&tracked, "llm_conditional_global"); assert_middleware_callback_locks_are_free(); - Ok(None) + ready(None) }), ) .unwrap(); @@ -2598,7 +2624,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_| { record_middleware_callback(&tracked, "llm_conditional_scope"); assert_middleware_callback_locks_are_free(); - Ok(None) + ready(None) }), ) .unwrap(); @@ -2610,7 +2636,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, request, annotated| { record_middleware_callback(&tracked, "llm_request_global"); assert_middleware_callback_locks_are_free(); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( request, annotated, )) }), @@ -2625,7 +2651,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |_, request, annotated| { record_middleware_callback(&tracked, "llm_request_scope"); assert_middleware_callback_locks_are_free(); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( request, annotated, )) }), @@ -2638,7 +2664,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |request, _context| { record_middleware_callback(&tracked, "llm_sanitize_request_global"); assert_middleware_callback_locks_are_free(); - Some(request) + ready(Some(request)) }), ) .unwrap(); @@ -2650,7 +2676,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |request, _context| { record_middleware_callback(&tracked, "llm_sanitize_request_scope"); assert_middleware_callback_locks_are_free(); - Some(request) + ready(Some(request)) }), ) .unwrap(); @@ -2707,7 +2733,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |response, _context| { record_middleware_callback(&tracked, "llm_sanitize_response_global"); assert_middleware_callback_locks_are_free(); - Some(response) + ready(Some(response)) }), ) .unwrap(); @@ -2719,7 +2745,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { Arc::new(move |response, _context| { record_middleware_callback(&tracked, "llm_sanitize_response_scope"); assert_middleware_callback_locks_are_free(); - Some(response) + ready(Some(response)) }), ) .unwrap(); @@ -2782,6 +2808,7 @@ async fn test_llm_middleware_callbacks_run_without_registry_or_scope_locks() { while let Some(chunk) = stream.next().await { chunk.unwrap(); } + stream.close().await.unwrap(); assert_middleware_callback_labels( &callbacks, &[ @@ -2852,7 +2879,7 @@ async fn test_full_pipeline_integration() { args.as_object_mut() .unwrap() .insert("intercepted".into(), json!(true)); - Ok(args) + ready(args) }), ) .unwrap(); @@ -2864,7 +2891,7 @@ async fn test_full_pipeline_integration() { 1, Arc::new(move |_name, args| { o2.lock().unwrap().push("sanitize_request".into()); - args + ready(args) }), ) .unwrap(); @@ -2876,7 +2903,7 @@ async fn test_full_pipeline_integration() { 1, Arc::new(move |_name, _args| { o3.lock().unwrap().push("conditional".into()); - Ok(None) // Allow + ready(None) // Allow }), ) .unwrap(); @@ -2903,7 +2930,7 @@ async fn test_full_pipeline_integration() { 1, Arc::new(move |_name, result| { o5.lock().unwrap().push("sanitize_response".into()); - result + ready(result) }), ) .unwrap(); @@ -2961,15 +2988,23 @@ async fn test_full_pipeline_integration() { // ========================================================================= /// Attempting to register a guardrail with the same name returns AlreadyExists. -#[test] -fn test_duplicate_guardrail_registration_returns_error() { +#[tokio::test] +async fn test_duplicate_guardrail_registration_returns_error() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); - register_tool_sanitize_request_guardrail("duplicate", 1, Arc::new(|_name, args| args)).unwrap(); + register_tool_sanitize_request_guardrail( + "duplicate", + 1, + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), + ) + .unwrap(); - let err = - register_tool_sanitize_request_guardrail("duplicate", 2, Arc::new(|_name, args| args)); + let err = register_tool_sanitize_request_guardrail( + "duplicate", + 2, + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), + ); assert!(err.is_err()); match err.unwrap_err() { @@ -2984,19 +3019,24 @@ fn test_duplicate_guardrail_registration_returns_error() { } /// Attempting to register an intercept with the same name returns AlreadyExists. -#[test] -fn test_duplicate_intercept_registration_returns_error() { +#[tokio::test] +async fn test_duplicate_intercept_registration_returns_error() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); - register_tool_request_intercept("dup_intercept", 1, false, Arc::new(|_name, args| Ok(args))) - .unwrap(); + register_tool_request_intercept( + "dup_intercept", + 1, + false, + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), + ) + .unwrap(); let err = register_tool_request_intercept( "dup_intercept", 2, false, - Arc::new(|_name, args| Ok(args)), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ); assert!(err.is_err()); @@ -3016,8 +3056,8 @@ fn test_duplicate_intercept_registration_returns_error() { // ========================================================================= /// Deregistering a non-existent guardrail returns false. -#[test] -fn test_deregister_nonexistent_returns_false() { +#[tokio::test] +async fn test_deregister_nonexistent_returns_false() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); @@ -3029,8 +3069,8 @@ fn test_deregister_nonexistent_returns_false() { } /// Deregistering removes the guardrail from the chain. -#[test] -fn test_deregister_removes_from_chain() { +#[tokio::test] +async fn test_deregister_removes_from_chain() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -3043,7 +3083,7 @@ fn test_deregister_removes_from_chain() { 1, Arc::new(move |_name, args| { cc.fetch_add(1, Ordering::SeqCst); - args + ready(args) }), ) .unwrap(); @@ -3056,6 +3096,7 @@ fn test_deregister_removes_from_chain() { .build(), ) .unwrap(); + flush_subscribers().unwrap(); assert_eq!(call_count.load(Ordering::SeqCst), 1); // Deregister @@ -3070,6 +3111,7 @@ fn test_deregister_removes_from_chain() { .build(), ) .unwrap(); + flush_subscribers().unwrap(); assert_eq!( call_count.load(Ordering::SeqCst), 1, @@ -3091,7 +3133,7 @@ async fn test_llm_conditional_guardrail_rejects() { register_llm_conditional_execution_guardrail( "llm_gate", 1, - Arc::new(|_req| Ok(Some("LLM call rejected".to_string()))), + Arc::new(|_req| ready(Some("LLM call rejected".to_string()))), ) .unwrap(); @@ -3142,12 +3184,12 @@ async fn test_llm_conditional_guardrail_emits_guardrail_scope() { ) .unwrap(); - register_llm_conditional_execution_guardrail("llm_scope_allow", 1, Arc::new(|_| Ok(None))) + register_llm_conditional_execution_guardrail("llm_scope_allow", 1, Arc::new(|_| ready(None))) .unwrap(); register_llm_conditional_execution_guardrail( "llm_scope_reject", 2, - Arc::new(|_| Ok(Some("blocked by llm guardrail".to_string()))), + Arc::new(|_| ready(Some("blocked by llm guardrail".to_string()))), ) .unwrap(); @@ -3236,9 +3278,9 @@ async fn test_llm_request_intercept_transforms() { "llm_req_i", 1, false, - Arc::new(|_name: &str, mut req: LlmRequest, annotated| { + Arc::new(|_name: String, mut req: LlmRequest, annotated| { req.headers.insert("x-intercepted".into(), json!(true)); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -3250,15 +3292,15 @@ async fn test_llm_request_intercept_transforms() { content: json!({"prompt": "hello"}), }; - let result = llm_request_intercepts("test_llm", request).unwrap(); + let result = llm_request_intercepts("test_llm", request).await.unwrap(); assert_eq!(result.request.headers["x-intercepted"], true); // Cleanup deregister_llm_request_intercept("llm_req_i").unwrap(); } -#[test] -fn test_llm_request_intercept_pending_marks_preserve_order_and_break_chain() { +#[tokio::test] +async fn test_llm_request_intercept_pending_marks_preserve_order_and_break_chain() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -3273,8 +3315,10 @@ fn test_llm_request_intercept_pending_marks_preserve_order_and_break_chain() { priority, break_chain, Arc::new(move |_name, request, annotated| { - Ok(LlmRequestInterceptOutcome::new(request, annotated) - .with_pending_mark(PendingMarkSpec::builder().name(mark_name).build())) + ready( + LlmRequestInterceptOutcome::new(request, annotated) + .with_pending_mark(PendingMarkSpec::builder().name(mark_name).build()), + ) }), ) .unwrap(); @@ -3287,6 +3331,7 @@ fn test_llm_request_intercept_pending_marks_preserve_order_and_break_chain() { content: json!({"prompt": "hello"}), }, ) + .await .unwrap(); assert_eq!( @@ -3322,7 +3367,7 @@ async fn test_managed_llm_emits_pending_marks_under_started_scope() { 1, Arc::new(|event, mut fields| { fields.metadata = Some(json!({"sanitized_mark": event.name()})); - fields + ready(fields) }), ) .unwrap(); @@ -3331,24 +3376,26 @@ async fn test_managed_llm_emits_pending_marks_under_started_scope() { 1, false, Arc::new(|_name, request, annotated| { - Ok(LlmRequestInterceptOutcome::new(request, annotated) - .with_pending_mark( - PendingMarkSpec::builder() - .name("request.optimized") - .category(EventCategory::custom()) - .category_profile( - CategoryProfile::builder() - .subtype("optimizer.saved_tokens") - .build(), - ) - .data(json!({"saved_tokens": 12})) - .build(), - ) - .with_pending_mark( - PendingMarkSpec::builder() - .name("request.optimized.second") - .build(), - )) + ready( + LlmRequestInterceptOutcome::new(request, annotated) + .with_pending_mark( + PendingMarkSpec::builder() + .name("request.optimized") + .category(EventCategory::custom()) + .category_profile( + CategoryProfile::builder() + .subtype("optimizer.saved_tokens") + .build(), + ) + .data(json!({"saved_tokens": 12})) + .build(), + ) + .with_pending_mark( + PendingMarkSpec::builder() + .name("request.optimized.second") + .build(), + ), + ) }), ) .unwrap(); @@ -3449,7 +3496,7 @@ async fn test_managed_llm_materializes_optimization_mark_and_end_summary() { data.insert("payload".to_string(), json!({"secret": "[redacted]"})); data.remove("future_secret"); } - fields + ready(fields) }), ) .unwrap(); @@ -3466,7 +3513,7 @@ async fn test_managed_llm_materializes_optimization_mark_and_end_summary() { contribution.payload = Some(json!({"secret": "[scope-end-redacted]"})); contribution.extra.remove("future_secret"); } - fields + ready(fields) }), ) .unwrap(); @@ -3492,8 +3539,10 @@ async fn test_managed_llm_materializes_optimization_mark_and_end_summary() { contribution .extra .insert("future_secret".to_string(), json!("classified")); - Ok(LlmRequestInterceptOutcome::new(request, annotated) - .with_optimization_contribution(contribution)) + ready( + LlmRequestInterceptOutcome::new(request, annotated) + .with_optimization_contribution(contribution), + ) }), ) .unwrap(); @@ -3698,7 +3747,7 @@ async fn test_stream_optimization_mark_uses_the_llm_captured_sanitizer_scope() { { data.insert("payload".to_string(), json!({"secret": "[redacted]"})); } - fields + ready(fields) }), ) .unwrap(); @@ -3753,6 +3802,7 @@ async fn test_stream_optimization_mark_uses_the_llm_captured_sanitizer_scope() { while let Some(item) = stream.next().await { item.unwrap(); } + stream.close().await.unwrap(); set_thread_scope_stack(original_stack); let captured = captured_events_snapshot(&events); @@ -3798,8 +3848,10 @@ async fn test_concurrent_managed_llm_calls_keep_optimization_evidence_isolated() saved: Some(LlmOptimizationTokens::saved_prompt(saved_tokens)), ..LlmOptimizationTokenImpact::default() }); - Ok(LlmRequestInterceptOutcome::new(request, annotated) - .with_optimization_contribution(contribution)) + ready( + LlmRequestInterceptOutcome::new(request, annotated) + .with_optimization_contribution(contribution), + ) }), ) .unwrap(); @@ -3890,8 +3942,10 @@ async fn test_failed_request_intercept_does_not_emit_pending_marks_or_start_scop 1, false, Arc::new(|_name, request, annotated| { - Ok(LlmRequestInterceptOutcome::new(request, annotated) - .with_pending_mark(PendingMarkSpec::builder().name("must.not.emit").build())) + ready( + LlmRequestInterceptOutcome::new(request, annotated) + .with_pending_mark(PendingMarkSpec::builder().name("must.not.emit").build()), + ) }), ) .unwrap(); @@ -3900,7 +3954,7 @@ async fn test_failed_request_intercept_does_not_emit_pending_marks_or_start_scop 2, false, Arc::new(|_name, _request, _annotated| { - Err(FlowError::Internal("request intercept failed".into())) + ready_result(Err(FlowError::Internal("request intercept failed".into()))) }), ) .unwrap(); @@ -4015,7 +4069,7 @@ async fn test_llm_start_emits_before_short_circuit_execution_intercept() { .as_object_mut() .unwrap() .insert("phase".into(), json!("request")); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -4109,7 +4163,7 @@ async fn test_llm_stream_start_emits_before_short_circuit_execution_intercept() .as_object_mut() .unwrap() .insert("phase".into(), json!("request")); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -4162,6 +4216,7 @@ async fn test_llm_stream_start_emits_before_short_circuit_execution_intercept() while let Some(chunk) = stream.next().await { chunk.unwrap(); } + stream.close().await.unwrap(); assert!( !original_called.load(Ordering::SeqCst), @@ -4190,19 +4245,19 @@ async fn test_llm_stream_start_emits_before_short_circuit_execution_intercept() // ========================================================================= /// tool_conditional_execution returns Ok(()) when no guardrails reject. -#[test] -fn test_standalone_conditional_execution_passes() { +#[tokio::test] +async fn test_standalone_conditional_execution_passes() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); - let result = tool_conditional_execution("tool", &json!({})); + let result = tool_conditional_execution("tool", &json!({})).await; assert!(result.is_ok(), "No guardrails means no rejection"); } /// tool_conditional_execution returns GuardrailRejected when a guardrail rejects. -#[test] -fn test_standalone_conditional_execution_rejects() { +#[tokio::test] +async fn test_standalone_conditional_execution_rejects() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); @@ -4210,11 +4265,11 @@ fn test_standalone_conditional_execution_rejects() { register_tool_conditional_execution_guardrail( "standalone_gate", 1, - Arc::new(|_name, _args| Ok(Some("rejected by standalone".to_string()))), + Arc::new(|_name, _args| Box::pin(async { Ok(Some("rejected by standalone".to_string())) })), ) .unwrap(); - let result = tool_conditional_execution("tool", &json!({})); + let result = tool_conditional_execution("tool", &json!({})).await; assert!(result.is_err()); match result.unwrap_err() { FlowError::GuardrailRejected(reason) => { @@ -4257,12 +4312,14 @@ async fn test_empty_chain_passthrough() { } /// Standalone intercept chain with no registrations returns input unchanged. -#[test] -fn test_empty_request_intercept_chain() { +#[tokio::test] +async fn test_empty_request_intercept_chain() { let _lock = TEST_MUTEX.lock().unwrap(); reset_global(); setup_isolated_thread(); - let result = tool_request_intercepts("tool", json!({"key": "val"})).unwrap(); + let result = tool_request_intercepts("tool", json!({"key": "val"})) + .await + .unwrap(); assert_eq!(result["key"], "val"); } diff --git a/crates/core/tests/integration/native_plugin_tests.rs b/crates/core/tests/integration/native_plugin_tests.rs index ee8a860be..c669f7c46 100644 --- a/crates/core/tests/integration/native_plugin_tests.rs +++ b/crates/core/tests/integration/native_plugin_tests.rs @@ -239,6 +239,7 @@ async fn sdk_cdylib_registers_tool_request_intercept() { .expect("outer scope should push"); let outer_uuid = outer.uuid; let rewritten = tool_request_intercepts("demo_tool", json!({ "input": "value" })) + .await .expect("native request intercept should run"); let tool_result = tool_call_execute( ToolCallExecuteParams::builder() @@ -446,6 +447,7 @@ async fn sdk_cdylib_registers_tool_request_intercept() { .expect("thread outer scope should push"); let thread_outer_uuid = thread_outer.uuid; let rewritten = tool_request_intercepts("demo_tool", json!({ "input": "thread" })) + .await .expect("native request intercept should run with thread stack"); assert_eq!(rewritten["native_plugin"], true); pop_scope( @@ -656,6 +658,123 @@ async fn sdk_cdylib_registers_tool_request_intercept() { activation.clear(); } +#[tokio::test] +async fn native_v3_async_registration_supports_all_middleware_kinds() { + let _guard = NATIVE_PLUGIN_TEST_LOCK.lock().await; + let fixture = build_fixture_plugin(); + let manifest_ref = write_manifest_with_plugin_id_and_symbol( + &fixture, + "fixture_async", + "nemo_relay_fixture_async_entry", + ); + + let activation = load_native_plugins([NativePluginLoadSpec { + plugin_id: "fixture_async".into(), + manifest_ref: manifest_ref.to_string_lossy().into_owned(), + }]) + .expect("v3 async native fixture should load"); + let mut config = PluginConfig::default(); + config.components.push(PluginComponentSpec { + kind: "fixture_async".into(), + enabled: true, + config: Map::new(), + }); + initialize_plugins_exact(config) + .await + .expect("v3 async native fixture should register"); + + let rewritten = tool_request_intercepts("async-tool", json!({"input": true})) + .await + .expect("v3 async request intercept should settle"); + assert_eq!(rewritten["input"], true); + assert_eq!(rewritten["native_async"], true); + + let duplicate = tool_request_intercepts("async-double", json!({"input": true})) + .await + .expect("duplicate v3 async settlement keeps the first result"); + assert_eq!(duplicate["native_async"], true); + + let executed = tool_call_execute( + ToolCallExecuteParams::builder() + .name("async-execution") + .args(json!({"input": true})) + .func(Arc::new(|args| Box::pin(async move { Ok(args) }))) + .build(), + ) + .await + .expect("v3 async execution intercept should continue with next"); + assert_eq!(executed["native_async_execution"], true); + + let llm_response = llm_call_execute( + LlmCallExecuteParams::builder() + .name("async-llm") + .request(LlmRequest { + headers: Map::new(), + content: json!({"prompt": "native async"}), + }) + .func(Arc::new(|_request| { + Box::pin(async move { Ok(json!({"content": "native async response"})) }) + })) + .build(), + ) + .await + .expect("v3 async LLM middleware should settle"); + assert_eq!(llm_response["content"], "native async response"); + flush_subscribers().expect("async native LLM events should flush"); + + let stream_chunks = Arc::new(Mutex::new(Vec::::new())); + let collected_chunks = stream_chunks.clone(); + let finalized_chunks = stream_chunks.clone(); + let mut stream = llm_stream_call_execute( + LlmStreamCallExecuteParams::builder() + .name("async-llm-stream") + .request(LlmRequest { + headers: Map::new(), + content: json!({"prompt": "native async stream"}), + }) + .func(Arc::new(|_request| { + Box::pin(async move { + Ok(LlmJsonStream::new(tokio_stream::iter(vec![Ok(json!({ + "content": "native async stream response" + }))]))) + }) + })) + .collector(Box::new(move |chunk| { + collected_chunks.lock().unwrap().push(chunk); + Ok(()) + })) + .finalizer(Box::new(move || { + Json::Array(finalized_chunks.lock().unwrap().clone()) + })) + .build(), + ) + .await + .expect("v3 async LLM stream middleware should settle"); + assert_eq!( + stream + .next() + .await + .expect("stream should contain a chunk") + .expect("stream chunk should succeed")["content"], + "native async stream response" + ); + assert!(stream.next().await.is_none()); + flush_subscribers().expect("async native LLM stream events should flush"); + + let pending = tokio::spawn(async { + tool_request_intercepts("async-pending", json!({"input": true})).await + }); + tokio::task::yield_now().await; + clear_plugin_configuration().expect("v3 async native fixture should clear while pending"); + let pending = pending + .await + .expect("pending v3 async task should not panic") + .expect("pending v3 async request intercept should settle after clear"); + assert_eq!(pending["native_async"], true); + + drop(activation); +} + #[tokio::test] async fn native_validation_diagnostics_prevent_initialization() { let _guard = NATIVE_PLUGIN_TEST_LOCK.lock().await; @@ -752,7 +871,7 @@ async fn native_tool_execution_rejects_null_malformed_and_error_outcomes() { } #[tokio::test] -async fn native_event_sanitizer_callback_errors_clear_observability_fields() { +async fn native_event_sanitizer_callback_errors_preserve_observability_fields() { let _guard = NATIVE_PLUGIN_TEST_LOCK.lock().await; let fixture = build_fixture_plugin(); let manifest_ref = @@ -793,8 +912,8 @@ async fn native_event_sanitizer_callback_errors_clear_observability_fields() { let captured_events = events.lock().unwrap().clone(); let event = find_event(&captured_events, "native-event-sanitize-error", None); - assert_eq!(event.data(), None); - assert_eq!(event.metadata(), None); + assert_eq!(event.data(), Some(&json!({ "secret": true }))); + assert_eq!(event.metadata(), Some(&json!({ "secret": true }))); deregister_subscriber("native_event_sanitizer_error_capture") .expect("test subscriber should deregister"); @@ -1218,6 +1337,7 @@ async fn plugin_host_activation_owns_configuration_until_clear() { .any(|kind| kind == "fixture_native") ); let rewritten = tool_request_intercepts("host-owned-tool", json!({ "input": true })) + .await .expect("host-owned intercept should run"); assert_eq!(rewritten["native_plugin"], true); @@ -1238,6 +1358,7 @@ async fn plugin_host_activation_owns_configuration_until_clear() { .any(|kind| kind == "fixture_native") ); let unchanged = tool_request_intercepts("host-owned-tool", json!({ "input": true })) + .await .expect("cleared intercept chain should be empty"); assert_eq!(unchanged, json!({ "input": true })); } @@ -1393,6 +1514,7 @@ async fn plugin_host_clear_allows_an_in_flight_native_callback_to_finish() { .clear() .expect("host should clear while a callback snapshot remains in flight"); let unchanged = tool_request_intercepts("after-clear", json!({ "input": true })) + .await .expect("new calls should observe the cleared registries"); assert_eq!(unchanged, json!({ "input": true })); diff --git a/crates/core/tests/integration/pipeline_tests.rs b/crates/core/tests/integration/pipeline_tests.rs index 1bc192b4b..9711974de 100644 --- a/crates/core/tests/integration/pipeline_tests.rs +++ b/crates/core/tests/integration/pipeline_tests.rs @@ -9,6 +9,9 @@ use std::sync::{Arc, Mutex}; +mod test_support; +use test_support::ready; + use futures::StreamExt; use serde_json::json; @@ -359,7 +362,7 @@ async fn test_decode_runs_before_intercepts() { false, Arc::new(move |_name, req, annotated| { *cap.lock().unwrap() = Some(annotated.clone()); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -414,7 +417,7 @@ async fn test_encode_runs_after_intercepts() { let mut ann = annotated.unwrap(); ann.model = Some("modified".into()); req.headers.insert("x-codec-route".into(), json!("blue")); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, Some(ann), )) @@ -517,7 +520,7 @@ async fn anthropic_issue_501_round_trips_and_applies_annotated_edits() { request .headers .insert("x-annotation-seen".into(), json!("yes")); - Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + ready(LlmRequestInterceptOutcome::new(request, Some(annotated))) }), ) .unwrap(); @@ -571,8 +574,10 @@ async fn test_codec_rejects_raw_content_mutation_before_lifecycle() { false, Arc::new(|_name, mut request, annotated| { request.content["model"] = json!("raw-model-edit"); - Ok(LlmRequestInterceptOutcome::new(request, annotated) - .with_pending_mark(PendingMarkSpec::builder().name("must.not.emit").build())) + ready( + LlmRequestInterceptOutcome::new(request, annotated) + .with_pending_mark(PendingMarkSpec::builder().name("must.not.emit").build()), + ) }), ) .unwrap(); @@ -583,7 +588,7 @@ async fn test_codec_rejects_raw_content_mutation_before_lifecycle() { false, Arc::new(move |_name, request, annotated| { *later_called.lock().unwrap() = true; - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + ready(LlmRequestInterceptOutcome::new(request, annotated)) }), ) .unwrap(); @@ -630,7 +635,9 @@ async fn test_codec_rejects_missing_annotation_before_lifecycle() { "codec_missing_annotation", 1, false, - Arc::new(|_name, request, _annotated| Ok(LlmRequestInterceptOutcome::new(request, None))), + Arc::new(|_name, request, _annotated| { + ready(LlmRequestInterceptOutcome::new(request, None)) + }), ) .unwrap(); @@ -680,7 +687,7 @@ async fn test_stream_codec_rejects_raw_content_mutation_before_lifecycle() { false, Arc::new(|_name, mut request, annotated| { request.content["model"] = json!("raw-stream-edit"); - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + ready(LlmRequestInterceptOutcome::new(request, annotated)) }), ) .unwrap(); @@ -741,7 +748,7 @@ async fn test_annotated_intercept_receives_both() { false, Arc::new(move |_name, req, annotated| { *cp.lock().unwrap() = Some((req.clone(), annotated.clone())); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -799,7 +806,7 @@ async fn test_canonical_intercept_with_and_without_codec() { Arc::new(move |_name, mut req, annotated| { *lc1.lock().unwrap() = true; req.headers.insert("x-legacy".into(), json!("was-here")); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -845,7 +852,7 @@ async fn test_canonical_intercept_with_and_without_codec() { Arc::new(move |_name, mut req, annotated| { *lc2.lock().unwrap() = true; req.headers.insert("x-legacy-2".into(), json!("also-here")); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -903,7 +910,7 @@ async fn test_stream_path_also_decodes() { false, Arc::new(move |_name, req, annotated| { *ca.lock().unwrap() = Some(annotated.clone()); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -930,6 +937,7 @@ async fn test_stream_path_also_decodes() { // Consume the stream to trigger full pipeline while let Some(_chunk) = stream.next().await {} + stream.close().await.unwrap(); // Assert decode was called let dl = decode_log.lock().unwrap(); @@ -969,7 +977,7 @@ async fn test_shared_helper_both_paths() { false, Arc::new(move |_name, req, annotated| { *acc.lock().unwrap() += 1; - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -1048,7 +1056,7 @@ async fn test_explicit_codec_param_overrides() { if let Some(ref ann) = annotated { *cm.lock().unwrap() = ann.model.clone(); } - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -1097,7 +1105,7 @@ async fn test_encode_merge_not_replace() { Arc::new(|_name, req, annotated| { let mut ann = annotated.unwrap(); ann.model = Some("new_model".into()); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, Some(ann), )) @@ -1168,7 +1176,7 @@ async fn test_unified_chain_priority_order() { false, Arc::new(move |_name, req, annotated| { cl1.lock().unwrap().push("legacy_p10".into()); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -1183,7 +1191,7 @@ async fn test_unified_chain_priority_order() { false, Arc::new(move |_name, req, annotated| { cl2.lock().unwrap().push("annotated_p5".into()); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -1233,7 +1241,7 @@ async fn test_no_codec_annotated_intercept_receives_none() { false, Arc::new(move |_name, req, annotated| { *ca.lock().unwrap() = Some(annotated.clone()); - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( + ready(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( req, annotated, )) }), @@ -1448,7 +1456,7 @@ async fn test_response_codec_annotation_uses_sanitized_managed_response() { register_llm_sanitize_response_guardrail( "sanitize_resp_codec_annotation", 1, - Arc::new(|_response, _context| Some(make_openai_chat_response("Sanitized"))), + Arc::new(|_response, _context| ready(Some(make_openai_chat_response("Sanitized")))), ) .unwrap(); @@ -1649,10 +1657,10 @@ async fn test_request_codec_annotation_uses_sanitized_start_payload() { "sanitize_req_codec_annotation", 1, Arc::new(|request, _context| { - Some(LlmRequest { + ready(Some(LlmRequest { headers: request.headers, content: make_openai_chat_request("Sanitized").content, - }) + })) }), ) .unwrap(); @@ -1733,6 +1741,7 @@ async fn test_stream_response_codec_populates_annotated_response() { // Drain the stream to trigger finalization and END event while let Some(_chunk) = stream.next().await {} + stream.close().await.unwrap(); let captured = captured_events_snapshot(&events); let end_event = captured @@ -1818,6 +1827,7 @@ async fn managed_buffered_and_streaming_close_price_the_committed_route_not_resp while let Some(item) = stream.next().await { item.unwrap(); } + stream.close().await.unwrap(); llm_call_execute( LlmCallExecuteParams::builder() @@ -1916,7 +1926,7 @@ async fn test_stream_response_codec_annotation_uses_sanitized_aggregated_respons register_llm_sanitize_response_guardrail( "stream_sanitize_resp_codec_annotation", 1, - Arc::new(|_response, _context| Some(make_openai_chat_response("Sanitized"))), + Arc::new(|_response, _context| ready(Some(make_openai_chat_response("Sanitized")))), ) .unwrap(); @@ -1939,6 +1949,7 @@ async fn test_stream_response_codec_annotation_uses_sanitized_aggregated_respons .unwrap(); while let Some(_chunk) = stream.next().await {} + stream.close().await.unwrap(); let captured = captured_events_snapshot(&events); let end_event = captured diff --git a/crates/core/tests/integration/scope_local_tests.rs b/crates/core/tests/integration/scope_local_tests.rs index 551d39dd7..ceb17e821 100644 --- a/crates/core/tests/integration/scope_local_tests.rs +++ b/crates/core/tests/integration/scope_local_tests.rs @@ -84,7 +84,7 @@ fn test_scope_local_guardrail_registration_and_execution() { args.as_object_mut() .unwrap() .insert("scope_sanitized".into(), json!(true)); - args + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -166,7 +166,7 @@ async fn test_auto_cleanup_on_scope_pop() { args.as_object_mut() .unwrap() .insert("ephemeral".into(), json!(true)); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -234,7 +234,7 @@ async fn test_priority_merge_global_and_scope_local() { args.as_object_mut() .unwrap() .insert("p10".into(), json!(true)); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -250,7 +250,7 @@ async fn test_priority_merge_global_and_scope_local() { args.as_object_mut() .unwrap() .insert("p30".into(), json!(true)); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -267,7 +267,7 @@ async fn test_priority_merge_global_and_scope_local() { args.as_object_mut() .unwrap() .insert("p20".into(), json!(true)); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -326,7 +326,7 @@ fn test_name_coexistence_global_and_scope_local() { 1, Arc::new(move |_name, args| { c1.fetch_add(1, Ordering::SeqCst); - args + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -339,7 +339,7 @@ fn test_name_coexistence_global_and_scope_local() { 2, Arc::new(move |_name, args| { c2.fetch_add(1, Ordering::SeqCst); - args + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -354,6 +354,7 @@ fn test_name_coexistence_global_and_scope_local() { .unwrap(); // Both guardrails with the same name ran. + flush_subscribers().unwrap(); assert_eq!(count.load(Ordering::SeqCst), 2); // Cleanup @@ -400,7 +401,7 @@ async fn test_scope_isolation_between_stacks() { args.as_object_mut() .unwrap() .insert("agent".into(), json!("a")); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -426,7 +427,7 @@ async fn test_scope_isolation_between_stacks() { args.as_object_mut() .unwrap() .insert("agent".into(), json!("b")); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -506,7 +507,7 @@ async fn test_nested_scope_inheritance() { args.as_object_mut() .unwrap() .insert("global".into(), json!(true)); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -530,7 +531,7 @@ async fn test_nested_scope_inheritance() { args.as_object_mut() .unwrap() .insert("scope_a".into(), json!(true)); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -555,7 +556,7 @@ async fn test_nested_scope_inheritance() { args.as_object_mut() .unwrap() .insert("scope_b".into(), json!(true)); - Ok(args) + Box::pin(async move { Ok(args) }) }), ) .unwrap(); @@ -702,11 +703,13 @@ async fn test_scope_local_conditional_execution_guardrail() { "tool_blocker", 1, Arc::new(|name, _args| { - if name == "banned_tool" { - Ok(Some("banned_tool is not allowed in this scope".to_string())) - } else { - Ok(None) - } + Box::pin(async move { + if name == "banned_tool" { + Ok(Some("banned_tool is not allowed in this scope".to_string())) + } else { + Ok(None) + } + }) }), ) .unwrap(); diff --git a/crates/core/tests/integration/subscriber_dispatcher_tests.rs b/crates/core/tests/integration/subscriber_dispatcher_tests.rs index 2276dee6c..303ee112b 100644 --- a/crates/core/tests/integration/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/integration/subscriber_dispatcher_tests.rs @@ -7,7 +7,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, mpsc}; use std::time::Duration; -use nemo_relay::api::event::Event; use nemo_relay::api::registry::{ deregister_mark_sanitize_guardrail, register_mark_sanitize_guardrail, }; @@ -16,7 +15,7 @@ use nemo_relay::api::runtime::{ }; use nemo_relay::api::scope::{EmitMarkEventParams, event}; use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber}; -use serde_json::json; +use nemo_relay::error::FlowError; static TEST_MUTEX: Mutex<()> = Mutex::new(()); @@ -103,146 +102,107 @@ fn dispatcher_preserves_event_order() { } #[test] -fn mark_emission_snapshots_sanitizers_and_returns_before_they_finish() { +fn dispatcher_continues_after_subscriber_panic() { let _lock = TEST_MUTEX.lock().unwrap(); flush_subscribers().unwrap(); reset_global(); setup_isolated_thread(); - let (sanitizer_started_tx, sanitizer_started_rx) = mpsc::channel(); - let (release_tx, release_rx) = mpsc::channel(); - let release_rx = Arc::new(Mutex::new(release_rx)); - register_mark_sanitize_guardrail( - "blocking-mark-sanitizer", - 10, - Arc::new(move |_, mut fields| { - sanitizer_started_tx.send(()).unwrap(); - release_rx.lock().unwrap().recv().unwrap(); - fields.data = Some(json!({"sanitized": true})); - fields - }), - ) - .unwrap(); - - let observed = Arc::new(Mutex::new(Vec::::new())); + let observed = Arc::new(Mutex::new(Vec::new())); let observed_events = Arc::clone(&observed); register_subscriber( - "sanitized-mark-subscriber", - Arc::new(move |event| observed_events.lock().unwrap().push(event.clone())), + "panic-isolated-subscriber", + Arc::new(move |event| { + if event.name() == "panic-isolated" { + panic!("subscriber failed"); + } + observed_events + .lock() + .unwrap() + .push(event.name().to_string()); + }), ) .unwrap(); - let (returned_tx, returned_rx) = mpsc::channel(); - let event_thread = std::thread::spawn(move || { - emit_mark("queued-sanitizer"); - returned_tx.send(()).unwrap(); - }); - - sanitizer_started_rx - .recv_timeout(Duration::from_secs(1)) - .expect("sanitizer should start on the dispatcher thread"); - returned_rx - .recv_timeout(Duration::from_secs(1)) - .expect("mark emission should return while its sanitizer is blocked"); - - // Removing the global registration cannot affect the already-snapshotted - // publication chain. - deregister_mark_sanitize_guardrail("blocking-mark-sanitizer").unwrap(); - release_tx.send(()).unwrap(); - event_thread.join().unwrap(); + emit_mark("panic-isolated"); + emit_mark("after-panic"); flush_subscribers().unwrap(); + deregister_subscriber("panic-isolated-subscriber").unwrap(); - let events = observed.lock().unwrap(); - assert_eq!(events.len(), 1); - assert_eq!( - events[0].sanitize_fields().data, - Some(json!({"sanitized": true})) - ); - drop(events); - deregister_subscriber("sanitized-mark-subscriber").unwrap(); + assert_eq!(observed.lock().unwrap().as_slice(), ["after-panic"]); } #[test] -fn mark_emission_skips_sanitizers_without_subscribers() { +fn dispatcher_publishes_the_snapshot_when_an_async_sanitizer_fails() { let _lock = TEST_MUTEX.lock().unwrap(); flush_subscribers().unwrap(); reset_global(); setup_isolated_thread(); - let sanitizer_called = Arc::new(AtomicBool::new(false)); - let called = Arc::clone(&sanitizer_called); + let observed = Arc::new(Mutex::new(Vec::new())); + let observed_events = Arc::clone(&observed); + register_subscriber( + "fail-open-sanitizer-subscriber", + Arc::new(move |event| { + observed_events + .lock() + .unwrap() + .push(event.name().to_string()) + }), + ) + .unwrap(); register_mark_sanitize_guardrail( - "unused-mark-sanitizer", + "fail-open-mark-sanitizer", 10, - Arc::new(move |_, fields| { - called.store(true, Ordering::Release); - fields + Arc::new(|_, _| { + Box::pin(async { + Err(FlowError::Internal( + "intentional event-sanitizer failure".to_string(), + )) + }) }), ) .unwrap(); - emit_mark("no-subscribers"); + emit_mark("unsanitized-fallback"); flush_subscribers().unwrap(); - deregister_mark_sanitize_guardrail("unused-mark-sanitizer").unwrap(); - assert!(!sanitizer_called.load(Ordering::Acquire)); + assert_eq!( + observed.lock().unwrap().as_slice(), + ["unsanitized-fallback"] + ); + deregister_mark_sanitize_guardrail("fail-open-mark-sanitizer").unwrap(); + deregister_subscriber("fail-open-sanitizer-subscriber").unwrap(); } #[test] -fn sanitizer_panic_publishes_the_latest_valid_event() { +fn mark_emission_skips_sanitizers_without_subscribers() { let _lock = TEST_MUTEX.lock().unwrap(); flush_subscribers().unwrap(); reset_global(); setup_isolated_thread(); + let sanitizer_called = Arc::new(AtomicBool::new(false)); + let called = Arc::clone(&sanitizer_called); register_mark_sanitize_guardrail( - "successful-mark-sanitizer", - 0, - Arc::new(move |_, mut fields| { - fields.data = Some(json!({"redacted": true})); - fields - }), - ) - .unwrap(); - register_mark_sanitize_guardrail( - "panicking-mark-sanitizer", + "unused-mark-sanitizer", 10, - Arc::new(move |_, _| panic!("sanitizer failed")), - ) - .unwrap(); - - let observed = Arc::new(Mutex::new(Vec::::new())); - let observed_events = Arc::clone(&observed); - register_subscriber( - "panic-fallback-subscriber", - Arc::new(move |event| observed_events.lock().unwrap().push(event.clone())), + Arc::new(move |_, fields| { + called.store(true, Ordering::Release); + Box::pin(async move { Ok(fields) }) + }), ) .unwrap(); - event( - EmitMarkEventParams::builder() - .name("panic-fallback") - .data(json!({"original": true})) - .build(), - ) - .unwrap(); + emit_mark("no-subscribers"); flush_subscribers().unwrap(); + deregister_mark_sanitize_guardrail("unused-mark-sanitizer").unwrap(); - deregister_mark_sanitize_guardrail("successful-mark-sanitizer").unwrap(); - deregister_mark_sanitize_guardrail("panicking-mark-sanitizer").unwrap(); - deregister_subscriber("panic-fallback-subscriber").unwrap(); - - let events = observed.lock().unwrap(); - assert_eq!(events.len(), 1); - assert_eq!(events[0].name(), "panic-fallback"); - assert_eq!( - events[0].sanitize_fields().data, - Some(json!({"redacted": true})) - ); + assert!(!sanitizer_called.load(Ordering::Acquire)); } #[test] -fn dispatcher_continues_after_subscriber_panic() { +fn dispatcher_publishes_the_snapshot_when_an_async_sanitizer_panics() { let _lock = TEST_MUTEX.lock().unwrap(); flush_subscribers().unwrap(); reset_global(); @@ -251,23 +211,26 @@ fn dispatcher_continues_after_subscriber_panic() { let observed = Arc::new(Mutex::new(Vec::new())); let observed_events = Arc::clone(&observed); register_subscriber( - "panic-isolated-subscriber", + "panic-sanitizer-subscriber", Arc::new(move |event| { - if event.name() == "panic-isolated" { - panic!("subscriber failed"); - } observed_events .lock() .unwrap() - .push(event.name().to_string()); + .push(event.name().to_string()) }), ) .unwrap(); + register_mark_sanitize_guardrail( + "panic-mark-sanitizer", + 10, + Arc::new(|_, _| Box::pin(async { panic!("intentional event-sanitizer panic") })), + ) + .unwrap(); - emit_mark("panic-isolated"); - emit_mark("after-panic"); + emit_mark("panic-fallback"); flush_subscribers().unwrap(); - deregister_subscriber("panic-isolated-subscriber").unwrap(); - assert_eq!(observed.lock().unwrap().as_slice(), ["after-panic"]); + assert_eq!(observed.lock().unwrap().as_slice(), ["panic-fallback"]); + deregister_mark_sanitize_guardrail("panic-mark-sanitizer").unwrap(); + deregister_subscriber("panic-sanitizer-subscriber").unwrap(); } diff --git a/crates/core/tests/integration/test_support.rs b/crates/core/tests/integration/test_support.rs new file mode 100644 index 000000000..bf1507f0f --- /dev/null +++ b/crates/core/tests/integration/test_support.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::future::Future; +use std::pin::Pin; + +pub fn ready( + value: T, +) -> Pin> + Send>> { + Box::pin(async move { Ok(value) }) +} + +#[allow(dead_code)] +pub fn ready_result( + value: nemo_relay::error::Result, +) -> Pin> + Send>> { + Box::pin(async move { value }) +} diff --git a/crates/core/tests/integration/worker_plugin_tests.rs b/crates/core/tests/integration/worker_plugin_tests.rs index 4387b048a..b7eb75611 100644 --- a/crates/core/tests/integration/worker_plugin_tests.rs +++ b/crates/core/tests/integration/worker_plugin_tests.rs @@ -76,6 +76,7 @@ async fn plugin_host_activation_owns_worker_lifecycle() { .any(|kind| kind == "fixture_worker") ); let rewritten = tool_request_intercepts("worker-host-tool", json!({ "input": true })) + .await .expect("worker host intercept should run"); assert_eq!(rewritten["worker_plugin"], true); @@ -86,6 +87,7 @@ async fn plugin_host_activation_owns_worker_lifecycle() { .any(|kind| kind == "fixture_worker") ); let unchanged = tool_request_intercepts("worker-host-tool", json!({ "input": true })) + .await .expect("cleared worker intercept chain should be empty"); assert_eq!(unchanged, json!({ "input": true })); } @@ -109,6 +111,7 @@ async fn plugin_host_clear_surfaces_worker_shutdown_failure_and_releases_safe_ow .expect("worker plugin host should activate"); tool_request_intercepts("terminate-worker", json!({ "input": true })) + .await .expect_err("fixture worker should terminate during callback"); let error = activation .clear() @@ -159,6 +162,7 @@ async fn rust_worker_registers_and_invokes_all_current_surfaces() { .expect("outer scope should push"); let outer_uuid = outer.uuid; let rewritten = tool_request_intercepts("demo_tool", json!({ "input": "value" })) + .await .expect("worker request intercept should run"); let tool_result = tool_call_execute( ToolCallExecuteParams::builder() @@ -473,6 +477,7 @@ async fn worker_request_intercept_callback_error_surfaces_to_host() { .await; let error = tool_request_intercepts("demo_tool", json!({ "input": "value" })) + .await .expect_err("worker callback error should surface"); assert!( error @@ -1120,6 +1125,7 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() { cleanup.subscriber_name = Some(subscriber_name); let rewritten = tool_request_intercepts("lookup", json!({ "query": "relay" })) + .await .expect("Python callback should emit a mark and return its mutation"); assert_eq!( rewritten["_nemo_relay_plugin"]["tag"], diff --git a/crates/core/tests/unit/context_tests.rs b/crates/core/tests/unit/context_tests.rs index 49e57b2cb..48853939d 100644 --- a/crates/core/tests/unit/context_tests.rs +++ b/crates/core/tests/unit/context_tests.rs @@ -46,7 +46,7 @@ fn scope_stack_tracks_scope_local_registries_and_subscribers() { priority: 10, payload: RequestIntercept { break_chain: false, - callable: Arc::new(|_, value| Ok(value)), + callable: Arc::new(|_, value| Box::pin(async move { Ok(value) })), }, }) .unwrap(); @@ -221,15 +221,17 @@ fn merge_helpers_preserve_global_and_scope_local_priority_order() { assert_eq!(merged_exec, vec![("local", 1), ("global", 15)]); } -#[test] -fn conditional_guardrail_snapshots_keep_names_and_callbacks_after_deregister() { +#[tokio::test] +async fn conditional_guardrail_snapshots_keep_names_and_callbacks_after_deregister() { let mut state = NemoRelayContextState::new(); state .tool_conditional_execution_guardrails .register(Guardrail { name: "snapshot_guardrail".to_string(), priority: 1, - payload: Arc::new(|name, _args| Ok(Some(format!("{name} blocked")))), + payload: Arc::new(|name, _args| { + Box::pin(async move { Ok(Some(format!("{name} blocked"))) }) + }), }) .unwrap(); @@ -257,6 +259,7 @@ fn conditional_guardrail_snapshots_keep_names_and_callbacks_after_deregister() { None, None, ) + .await .unwrap(); assert_eq!(rejection.as_deref(), Some("snapshot_target blocked")); @@ -320,12 +323,14 @@ fn context_state_supports_extensions_events_and_builders() { content: json!({"messages": []}), }; let entries = state.llm_sanitize_request_entries(&[]); - let sanitized = NemoRelayContextState::llm_sanitize_request_snapshot_chain( - request.clone(), - crate::api::runtime::LlmSanitizeRequestContext::default(), - &entries, - ) - .expect("an empty sanitizer chain must retain the request"); + let sanitized = tokio::runtime::Runtime::new() + .unwrap() + .block_on(NemoRelayContextState::llm_sanitize_request_snapshot_chain( + request.clone(), + crate::api::runtime::LlmSanitizeRequestContext::default(), + &entries, + )) + .expect("an empty sanitizer chain must retain the request"); assert!(sanitized.headers.is_empty()); let events = Arc::new(Mutex::new(Vec::::new())); diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index 7754238dc..da3d9da7e 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -461,6 +461,7 @@ async fn callback_helpers_cover_worker_response_edges() { RegistrationSurface::MarkSanitizeGuardrail, &event, ) + .await .expect_err("invalid event sanitizer fields should fail"); assert!( error @@ -474,6 +475,7 @@ async fn callback_helpers_cover_worker_response_edges() { valid_llm_request(), LlmSanitizeRequestContext::default(), ) + .await .expect_err("invalid LLM JSON result should fail"); assert!(error.to_string().contains("invalid type")); @@ -484,6 +486,7 @@ async fn callback_helpers_cover_worker_response_edges() { valid_llm_request(), None, ) + .await .expect_err("invalid LLM intercept request should fail"); assert!( error @@ -498,6 +501,7 @@ async fn callback_helpers_cover_worker_response_edges() { valid_llm_request(), None, ) + .await .expect_err("legacy outcome schema should fail"); assert!( error @@ -512,6 +516,7 @@ async fn callback_helpers_cover_worker_response_edges() { valid_llm_request(), None, ) + .await .expect_err("invalid annotated request should fail"); assert!( error @@ -521,6 +526,7 @@ async fn callback_helpers_cover_worker_response_edges() { let error = callback .invoke_llm_request_intercept("llm_intercept_error", "model", valid_llm_request(), None) + .await .expect_err("LLM intercept worker error should surface"); assert!(error.to_string().contains("worker.failed: boom")); @@ -531,6 +537,7 @@ async fn callback_helpers_cover_worker_response_edges() { valid_llm_request(), None, ) + .await .expect_err("unexpected LLM intercept result should fail"); assert!( error @@ -586,6 +593,7 @@ async fn llm_worker_sanitizers_forward_codec_context_and_omission() { valid_llm_request(), LlmSanitizeRequestContext::with_identity(identity.clone()), ) + .await .expect("empty worker result must represent request omission") .is_none() ); @@ -596,6 +604,7 @@ async fn llm_worker_sanitizers_forward_codec_context_and_omission() { json!({"secret": "value"}), LlmSanitizeResponseContext::with_identity(identity), ) + .await .expect("empty worker result must represent response omission") .is_none() ); @@ -774,6 +783,7 @@ async fn llm_worker_codec_capabilities_are_active_only_during_sanitizer_invocati request, LlmSanitizeRequestContext::for_request_codec(Some(codec.clone())), ) + .await .expect("request sanitizer must succeed") .is_none() ); @@ -793,6 +803,7 @@ async fn llm_worker_codec_capabilities_are_active_only_during_sanitizer_invocati response, LlmSanitizeResponseContext::for_response_codec(Some(codec)), ) + .await .expect_err("worker sanitizer error must surface"); assert!(error.to_string().contains("worker.failed: boom")); @@ -1324,6 +1335,7 @@ async fn install_registrations_covers_registry_error_edges() { } #[tokio::test(flavor = "multi_thread")] +#[allow(clippy::await_holding_lock)] // Serializes access to global runtime state. async fn installed_callbacks_apply_surface_specific_fallbacks() { struct RuntimeCleanup { registrations: Option, @@ -1423,9 +1435,9 @@ async fn installed_callbacks_apply_surface_specific_fallbacks() { ] { let entries = NemoRelayContextState::event_sanitize_entries(registry, &[]); let sanitized = - NemoRelayContextState::event_sanitize_snapshot_chain(event.clone(), &entries); - assert_eq!(sanitized.data(), None); - assert_eq!(sanitized.metadata(), None); + NemoRelayContextState::event_sanitize_snapshot_chain(event.clone(), &entries).await; + assert_eq!(sanitized.data(), event.data()); + assert_eq!(sanitized.metadata(), event.metadata()); } let entries = state.tool_sanitize_request_entries(&[]); @@ -1434,7 +1446,8 @@ async fn installed_callbacks_apply_surface_specific_fallbacks() { "tool", tool_request.clone(), &entries, - ), + ) + .await, tool_request ); let entries = state.tool_sanitize_response_entries(&[]); @@ -1443,28 +1456,29 @@ async fn installed_callbacks_apply_surface_specific_fallbacks() { "tool", tool_response.clone(), &entries, - ), + ) + .await, tool_response ); let entries = state.llm_sanitize_request_entries(&[]); - assert!( + assert_eq!( NemoRelayContextState::llm_sanitize_request_snapshot_chain( llm_request.clone(), crate::api::runtime::LlmSanitizeRequestContext::default(), &entries, ) - .is_none(), - "a worker request sanitizer failure must omit the observability payload" + .await, + Some(llm_request), ); let entries = state.llm_sanitize_response_entries(&[]); - assert!( + assert_eq!( NemoRelayContextState::llm_sanitize_response_snapshot_chain( llm_response.clone(), crate::api::runtime::LlmSanitizeResponseContext::default(), &entries, ) - .is_none(), - "a worker response sanitizer failure must omit the observability payload" + .await, + Some(llm_response), ); } crate::api::subscriber::flush_subscribers().expect("subscriber callback should flush"); diff --git a/crates/core/tests/unit/llm_api_tests.rs b/crates/core/tests/unit/llm_api_tests.rs index 65c6e5b17..843fe4bf5 100644 --- a/crates/core/tests/unit/llm_api_tests.rs +++ b/crates/core/tests/unit/llm_api_tests.rs @@ -295,7 +295,7 @@ fn credential_headers_are_removed_before_request_sanitizers_and_event_emission() 1, Arc::new(move |request, _context| { sanitizer_capture.lock().unwrap().push(request.clone()); - Some(request) + Box::pin(async move { Ok(Some(request)) }) }), ) .unwrap(); @@ -431,13 +431,13 @@ fn sanitization_invalidates_manual_annotations_without_a_codec() { register_llm_sanitize_request_guardrail( "manual-annotation-invalidation-request", 1, - Arc::new(|_request, _context| Some(redacted_request())), + Arc::new(|_request, _context| Box::pin(async { Ok(Some(redacted_request())) })), ) .unwrap(); register_llm_sanitize_response_guardrail( "manual-annotation-invalidation-response", 1, - Arc::new(|_response, _context| Some(redacted_response())), + Arc::new(|_response, _context| Box::pin(async { Ok(Some(redacted_response())) })), ) .unwrap(); @@ -500,13 +500,13 @@ fn no_op_sanitizers_keep_manual_annotations() { register_llm_sanitize_request_guardrail( "manual-annotation-noop-request", 1, - Arc::new(|request, _context| Some(request)), + Arc::new(|request, _context| Box::pin(async move { Ok(Some(request)) })), ) .unwrap(); register_llm_sanitize_response_guardrail( "manual-annotation-noop-response", 1, - Arc::new(|response, _context| Some(response)), + Arc::new(|response, _context| Box::pin(async move { Ok(Some(response)) })), ) .unwrap(); @@ -558,13 +558,13 @@ fn sanitization_regenerates_annotations_with_active_codecs() { register_llm_sanitize_request_guardrail( "active-codec-annotation-regeneration-request", 1, - Arc::new(|_request, _context| Some(redacted_request())), + Arc::new(|_request, _context| Box::pin(async { Ok(Some(redacted_request())) })), ) .unwrap(); register_llm_sanitize_response_guardrail( "active-codec-annotation-regeneration-response", 1, - Arc::new(|_response, _context| Some(redacted_response())), + Arc::new(|_response, _context| Box::pin(async { Ok(Some(redacted_response())) })), ) .unwrap(); @@ -647,7 +647,7 @@ fn buffered_null_fallback_is_sanitized_before_emission() { 1, Arc::new(move |response, _context| { sanitizer_inputs.lock().unwrap().push(response); - Some(Json::Null) + Box::pin(async { Ok(Some(Json::Null)) }) }), ) .unwrap(); @@ -676,7 +676,7 @@ fn buffered_null_fallback_is_sanitized_before_emission() { register_llm_sanitize_response_guardrail( "buffered-null-fallback-redacted", 1, - Arc::new(|_response, _context| Some(redacted_response())), + Arc::new(|_response, _context| Box::pin(async { Ok(Some(redacted_response())) })), ) .unwrap(); let handle = create_llm_handle( @@ -760,7 +760,7 @@ fn streaming_null_fallback_is_sanitized_before_emission() { 1, Arc::new(move |response, _context| { sanitizer_inputs.lock().unwrap().push(response); - Some(Json::Null) + Box::pin(async { Ok(Some(Json::Null)) }) }), ) .unwrap(); @@ -791,7 +791,7 @@ fn streaming_null_fallback_is_sanitized_before_emission() { register_llm_sanitize_response_guardrail( "streaming-null-fallback-redacted", 1, - Arc::new(|_response, _context| Some(redacted_response())), + Arc::new(|_response, _context| Box::pin(async { Ok(Some(redacted_response())) })), ) .unwrap(); runtime.block_on(async { @@ -1525,11 +1525,12 @@ fn failed_managed_calls_sanitize_fallback_end_data() { "failed-managed-call-sanitization", 1, Arc::new(move |response, context| { - sanitizer_inputs - .lock() - .unwrap() - .push((response, context.codec().clone())); - Some(redacted_response()) + let codec = context.codec().clone(); + let sanitizer_inputs = Arc::clone(&sanitizer_inputs); + Box::pin(async move { + sanitizer_inputs.lock().unwrap().push((response, codec)); + Ok(Some(redacted_response())) + }) }), ) .unwrap(); diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index beec626af..a29955c17 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -257,10 +257,135 @@ fn native_string_and_json_helpers_cover_abi_boundaries() { assert_eq!(host_api.abi_version, NEMO_RELAY_NATIVE_ABI_VERSION); assert_eq!( host_api.struct_size, - std::mem::size_of::() + std::mem::size_of::() ); } +#[test] +fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let cases: Vec<(NativeAsyncNextInner, Json, Json)> = vec![ + ( + NativeAsyncNextInner::Tool(Arc::new(|value| Box::pin(async move { Ok(value) }))), + json!({"tool": true}), + json!({"result": {"tool": true}, "pending_marks": []}), + ), + ( + NativeAsyncNextInner::Llm(Arc::new(|request| { + Box::pin(async move { Ok(request.content) }) + })), + serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"llm": true}), + }) + .unwrap(), + json!({"llm": true}), + ), + ( + NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + Box::pin(async { + Ok(LlmJsonStream::new(tokio_stream::iter(vec![ + Ok(json!({"chunk": 1})), + Ok(json!({"chunk": 2})), + ]))) + }) + })), + serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"stream": true}), + }) + .unwrap(), + json!([{"chunk": 1}, {"chunk": 2}]), + ), + ]; + + for (inner, invocation, expected) in cases { + let next = Arc::new(NativeAsyncNext { + inner, + runtime: runtime.handle().clone(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let invocation = native_string_from_json(&invocation).unwrap(); + assert_eq!( + unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, + NemoRelayStatus::Ok + ); + assert_eq!(runtime.block_on(receiver).unwrap().unwrap(), expected); + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_completion_release(completion_ref); + } + } +} + +#[test] +fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlement() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let invalid = native_string("not-json"); + assert_eq!( + unsafe { native_async_completion_resolve_json(completion_ref, invalid) }, + NemoRelayStatus::InvalidJson + ); + let value = native_string(r#"{"ok":true}"#); + assert_eq!( + unsafe { native_async_completion_resolve_json(completion_ref, value) }, + NemoRelayStatus::Ok + ); + assert_eq!( + unsafe { native_async_completion_resolve_json(completion_ref, value) }, + NemoRelayStatus::InvalidArg + ); + assert_eq!( + runtime.block_on(receiver).unwrap().unwrap(), + json!({"ok": true}) + ); + unsafe { + native_string_free(invalid); + native_string_free(value); + native_async_completion_release(completion_ref); + } + + let (sender, _receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(true), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + assert!(unsafe { native_async_completion_is_cancelled(completion_ref) }); + assert!(unsafe { native_async_completion_is_cancelled(ptr::null()) }); + assert_eq!( + unsafe { native_async_completion_reject(completion_ref, ptr::null()) }, + NemoRelayStatus::InvalidArg + ); + unsafe { native_async_completion_release(completion_ref) }; +} + #[test] fn native_timestamp_scope_type_and_error_mappings_cover_variants() { assert_eq!(optional_timestamp_from_native(ptr::null()).unwrap(), None); diff --git a/crates/core/tests/unit/plugin_tests.rs b/crates/core/tests/unit/plugin_tests.rs index a0c7503cf..b16b3fb11 100644 --- a/crates/core/tests/unit/plugin_tests.rs +++ b/crates/core/tests/unit/plugin_tests.rs @@ -115,8 +115,10 @@ impl Plugin for TestPlugin { 1, false, Arc::new(|_name, mut request, annotated| { - request.headers.insert("x-plugin".into(), json!(true)); - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + Box::pin(async move { + request.headers.insert("x-plugin".into(), json!(true)); + Ok(LlmRequestInterceptOutcome::new(request, annotated)) + }) }), ) }) @@ -754,27 +756,29 @@ fn test_plugin_registration_context_registers_and_rolls_back() { .block_on(TestPlugin.register(&Map::new(), &mut ctx)) .unwrap(); - let request = llm_request_intercepts( - "model", - LlmRequest { - headers: Map::new(), - content: json!({"messages": []}), - }, - ) - .unwrap(); + let request = runtime + .block_on(llm_request_intercepts( + "model", + LlmRequest { + headers: Map::new(), + content: json!({"messages": []}), + }, + )) + .unwrap(); assert_eq!(request.request.headers.get("x-plugin"), Some(&json!(true))); let mut registrations = ctx.into_registrations(); rollback_registrations(&mut registrations); - let request = llm_request_intercepts( - "model", - LlmRequest { - headers: Map::new(), - content: json!({"messages": []}), - }, - ) - .unwrap(); + let request = runtime + .block_on(llm_request_intercepts( + "model", + LlmRequest { + headers: Map::new(), + content: json!({"messages": []}), + }, + )) + .unwrap(); assert_eq!(request.request.headers.get("x-plugin"), None); reset_global(); } @@ -798,25 +802,27 @@ fn test_initialize_plugins_registers_and_clears_components() { assert!(!report.has_errors()); assert!(active_plugin_report().is_some()); - let request = llm_request_intercepts( - "model", - LlmRequest { - headers: Map::new(), - content: json!({"messages": []}), - }, - ) - .unwrap(); + let request = runtime + .block_on(llm_request_intercepts( + "model", + LlmRequest { + headers: Map::new(), + content: json!({"messages": []}), + }, + )) + .unwrap(); assert_eq!(request.request.headers.get("x-plugin"), Some(&json!(true))); clear_plugin_configuration().unwrap(); - let request = llm_request_intercepts( - "model", - LlmRequest { - headers: Map::new(), - content: json!({"messages": []}), - }, - ) - .unwrap(); + let request = runtime + .block_on(llm_request_intercepts( + "model", + LlmRequest { + headers: Map::new(), + content: json!({"messages": []}), + }, + )) + .unwrap(); assert_eq!(request.request.headers.get("x-plugin"), None); reset_global(); } @@ -1070,8 +1076,13 @@ fn test_plugin_registration_context_covers_all_registration_helpers() { let mut ctx = PluginRegistrationContext::with_namespace("demo::"); ctx.register_subscriber("subscriber", Arc::new(|_event| {})) .unwrap(); - ctx.register_tool_request_intercept("tool-request", 1, false, Arc::new(|_name, args| Ok(args))) - .unwrap(); + ctx.register_tool_request_intercept( + "tool-request", + 1, + false, + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), + ) + .unwrap(); ctx.register_tool_execution_intercept( "tool-exec", 1, @@ -1083,7 +1094,7 @@ fn test_plugin_registration_context_covers_all_registration_helpers() { 1, false, Arc::new(|_name, request, annotated| { - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + Box::pin(async move { Ok(LlmRequestInterceptOutcome::new(request, annotated)) }) }), ) .unwrap(); @@ -1586,69 +1597,78 @@ fn test_plugin_registration_context_supports_guardrail_helpers() { reset_global(); let mut ctx = PluginRegistrationContext::with_namespace("plugin::"); - ctx.register_mark_sanitize_guardrail("mark_sanitize", 1, Arc::new(|_, fields| fields)) - .unwrap(); + ctx.register_mark_sanitize_guardrail( + "mark_sanitize", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap(); ctx.register_scope_sanitize_start_guardrail( "scope_sanitize_start", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); ctx.register_scope_sanitize_end_guardrail( "scope_sanitize_end", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); ctx.register_tool_sanitize_request_guardrail( "tool_sanitize_request", 1, - Arc::new(|_, args| args), + Arc::new(|_, args| Box::pin(async move { Ok(args) })), ) .unwrap(); ctx.register_tool_sanitize_response_guardrail( "tool_sanitize_response", 1, - Arc::new(|_, response| response), + Arc::new(|_, response| Box::pin(async move { Ok(response) })), ) .unwrap(); ctx.register_tool_conditional_execution_guardrail( "tool_conditional", 1, - Arc::new(|name, _args| Ok((name == "blocked-tool").then(|| "blocked tool".to_string()))), + Arc::new(|name, _args| { + Box::pin( + async move { Ok((name == "blocked-tool").then(|| "blocked tool".to_string())) }, + ) + }), ) .unwrap(); ctx.register_llm_sanitize_request_guardrail( "llm_sanitize_request", 1, - Arc::new(|request, _context| Some(request)), + Arc::new(|request, _context| Box::pin(async move { Ok(Some(request)) })), ) .unwrap(); ctx.register_llm_sanitize_response_guardrail( "llm_sanitize_response", 1, - Arc::new(|response, _context| Some(response)), + Arc::new(|response, _context| Box::pin(async move { Ok(Some(response)) })), ) .unwrap(); ctx.register_llm_conditional_execution_guardrail( "llm_conditional", 1, Arc::new(|request| { - Ok((request.headers.get("blocked") == Some(&json!(true))) - .then(|| "blocked llm".to_string())) + let blocked = request.headers.get("blocked") == Some(&json!(true)); + Box::pin(async move { Ok(blocked.then(|| "blocked llm".to_string())) }) }), ) .unwrap(); - match tool_conditional_execution("blocked-tool", &json!({})) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + match runtime.block_on(tool_conditional_execution("blocked-tool", &json!({}))) { Err(FlowError::GuardrailRejected(message)) => assert_eq!(message, "blocked tool"), other => panic!("expected tool guardrail rejection, got {other:?}"), } - match llm_conditional_execution(&LlmRequest { + match runtime.block_on(llm_conditional_execution(&LlmRequest { headers: Map::from_iter([(String::from("blocked"), json!(true))]), content: json!({"messages": []}), - }) { + })) { Err(FlowError::GuardrailRejected(message)) => assert_eq!(message, "blocked llm"), other => panic!("expected llm guardrail rejection, got {other:?}"), } @@ -1656,13 +1676,18 @@ fn test_plugin_registration_context_supports_guardrail_helpers() { let mut registrations = ctx.into_registrations(); rollback_registrations(&mut registrations); - assert!(tool_conditional_execution("blocked-tool", &json!({})).is_ok()); assert!( - llm_conditional_execution(&LlmRequest { - headers: Map::from_iter([(String::from("blocked"), json!(true))]), - content: json!({"messages": []}), - }) - .is_ok() + runtime + .block_on(tool_conditional_execution("blocked-tool", &json!({}))) + .is_ok() + ); + assert!( + runtime + .block_on(llm_conditional_execution(&LlmRequest { + headers: Map::from_iter([(String::from("blocked"), json!(true))]), + content: json!({"messages": []}), + })) + .is_ok() ); reset_global(); @@ -1674,22 +1699,46 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { reset_global(); let mut ctx = PluginRegistrationContext::with_namespace("duplicate::"); - ctx.register_mark_sanitize_guardrail("mark", 1, Arc::new(|_, fields| fields)) - .unwrap(); + ctx.register_mark_sanitize_guardrail( + "mark", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap(); expect_registration_failed( - ctx.register_mark_sanitize_guardrail("mark", 1, Arc::new(|_, fields| fields)), + ctx.register_mark_sanitize_guardrail( + "mark", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ), "mark sanitizer:", ); - ctx.register_scope_sanitize_start_guardrail("scope-start", 1, Arc::new(|_, fields| fields)) - .unwrap(); + ctx.register_scope_sanitize_start_guardrail( + "scope-start", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap(); expect_registration_failed( - ctx.register_scope_sanitize_start_guardrail("scope-start", 1, Arc::new(|_, fields| fields)), + ctx.register_scope_sanitize_start_guardrail( + "scope-start", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ), "scope-start sanitizer:", ); - ctx.register_scope_sanitize_end_guardrail("scope-end", 1, Arc::new(|_, fields| fields)) - .unwrap(); + ctx.register_scope_sanitize_end_guardrail( + "scope-end", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap(); expect_registration_failed( - ctx.register_scope_sanitize_end_guardrail("scope-end", 1, Arc::new(|_, fields| fields)), + ctx.register_scope_sanitize_end_guardrail( + "scope-end", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ), "scope-end sanitizer:", ); ctx.register_llm_request_intercept( @@ -1697,7 +1746,7 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { 1, false, Arc::new(|_name, request, annotated| { - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + Box::pin(async move { Ok(LlmRequestInterceptOutcome::new(request, annotated)) }) }), ) .unwrap(); @@ -1707,7 +1756,7 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { 1, false, Arc::new(|_name, request, annotated| { - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + Box::pin(async move { Ok(LlmRequestInterceptOutcome::new(request, annotated)) }) }), ), "llm request intercept:", @@ -1716,14 +1765,14 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { ctx.register_tool_sanitize_request_guardrail( "tool-sanitize-request", 1, - Arc::new(|_, args| args), + Arc::new(|_, args| Box::pin(async move { Ok(args) })), ) .unwrap(); expect_registration_failed( ctx.register_tool_sanitize_request_guardrail( "tool-sanitize-request", 1, - Arc::new(|_, args| args), + Arc::new(|_, args| Box::pin(async move { Ok(args) })), ), "tool sanitize request guardrail:", ); @@ -1731,14 +1780,14 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { ctx.register_tool_sanitize_response_guardrail( "tool-sanitize-response", 1, - Arc::new(|_, response| response), + Arc::new(|_, response| Box::pin(async move { Ok(response) })), ) .unwrap(); expect_registration_failed( ctx.register_tool_sanitize_response_guardrail( "tool-sanitize-response", 1, - Arc::new(|_, response| response), + Arc::new(|_, response| Box::pin(async move { Ok(response) })), ), "tool sanitize response guardrail:", ); @@ -1746,14 +1795,14 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { ctx.register_tool_conditional_execution_guardrail( "tool-conditional", 1, - Arc::new(|_, _| Ok(None)), + Arc::new(|_, _| Box::pin(async { Ok(None) })), ) .unwrap(); expect_registration_failed( ctx.register_tool_conditional_execution_guardrail( "tool-conditional", 1, - Arc::new(|_, _| Ok(None)), + Arc::new(|_, _| Box::pin(async { Ok(None) })), ), "tool conditional execution guardrail:", ); @@ -1761,14 +1810,14 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { ctx.register_llm_sanitize_request_guardrail( "llm-sanitize-request", 1, - Arc::new(|request, _context| Some(request)), + Arc::new(|request, _context| Box::pin(async move { Ok(Some(request)) })), ) .unwrap(); expect_registration_failed( ctx.register_llm_sanitize_request_guardrail( "llm-sanitize-request", 1, - Arc::new(|request, _context| Some(request)), + Arc::new(|request, _context| Box::pin(async move { Ok(Some(request)) })), ), "llm sanitize request guardrail:", ); @@ -1776,25 +1825,29 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { ctx.register_llm_sanitize_response_guardrail( "llm-sanitize-response", 1, - Arc::new(|response, _context| Some(response)), + Arc::new(|response, _context| Box::pin(async move { Ok(Some(response)) })), ) .unwrap(); expect_registration_failed( ctx.register_llm_sanitize_response_guardrail( "llm-sanitize-response", 1, - Arc::new(|response, _context| Some(response)), + Arc::new(|response, _context| Box::pin(async move { Ok(Some(response)) })), ), "llm sanitize response guardrail:", ); - ctx.register_llm_conditional_execution_guardrail("llm-conditional", 1, Arc::new(|_| Ok(None))) - .unwrap(); + ctx.register_llm_conditional_execution_guardrail( + "llm-conditional", + 1, + Arc::new(|_| Box::pin(async { Ok(None) })), + ) + .unwrap(); expect_registration_failed( ctx.register_llm_conditional_execution_guardrail( "llm-conditional", 1, - Arc::new(|_| Ok(None)), + Arc::new(|_| Box::pin(async { Ok(None) })), ), "llm conditional execution guardrail:", ); @@ -1841,14 +1894,19 @@ fn test_plugin_registration_context_maps_duplicate_registration_errors() { "llm stream execution intercept:", ); - ctx.register_tool_request_intercept("tool-request", 1, false, Arc::new(|_name, args| Ok(args))) - .unwrap(); + ctx.register_tool_request_intercept( + "tool-request", + 1, + false, + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), + ) + .unwrap(); expect_registration_failed( ctx.register_tool_request_intercept( "tool-request", 1, false, - Arc::new(|_name, args| Ok(args)), + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), ), "tool request intercept:", ); @@ -1879,18 +1937,22 @@ fn test_plugin_registration_context_maps_deregistration_errors() { reset_global(); let mut ctx = PluginRegistrationContext::with_namespace("teardown::"); - ctx.register_mark_sanitize_guardrail("mark-sanitize", 1, Arc::new(|_, fields| fields)) - .unwrap(); + ctx.register_mark_sanitize_guardrail( + "mark-sanitize", + 1, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) + .unwrap(); ctx.register_scope_sanitize_start_guardrail( "scope-sanitize-start", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); ctx.register_scope_sanitize_end_guardrail( "scope-sanitize-end", 1, - Arc::new(|_, fields| fields), + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), ) .unwrap(); ctx.register_subscriber("subscriber", Arc::new(|_event| {})) @@ -1900,42 +1962,46 @@ fn test_plugin_registration_context_maps_deregistration_errors() { 1, false, Arc::new(|_name, request, annotated| { - Ok(LlmRequestInterceptOutcome::new(request, annotated)) + Box::pin(async move { Ok(LlmRequestInterceptOutcome::new(request, annotated)) }) }), ) .unwrap(); ctx.register_tool_sanitize_request_guardrail( "tool-sanitize-request", 1, - Arc::new(|_, args| args), + Arc::new(|_, args| Box::pin(async move { Ok(args) })), ) .unwrap(); ctx.register_tool_sanitize_response_guardrail( "tool-sanitize-response", 1, - Arc::new(|_, response| response), + Arc::new(|_, response| Box::pin(async move { Ok(response) })), ) .unwrap(); ctx.register_tool_conditional_execution_guardrail( "tool-conditional", 1, - Arc::new(|_, _| Ok(None)), + Arc::new(|_, _| Box::pin(async { Ok(None) })), ) .unwrap(); ctx.register_llm_sanitize_request_guardrail( "llm-sanitize-request", 1, - Arc::new(|request, _context| Some(request)), + Arc::new(|request, _context| Box::pin(async move { Ok(Some(request)) })), ) .unwrap(); ctx.register_llm_sanitize_response_guardrail( "llm-sanitize-response", 1, - Arc::new(|response, _context| Some(response)), + Arc::new(|response, _context| Box::pin(async move { Ok(Some(response)) })), + ) + .unwrap(); + ctx.register_llm_conditional_execution_guardrail( + "llm-conditional", + 1, + Arc::new(|_| Box::pin(async { Ok(None) })), ) .unwrap(); - ctx.register_llm_conditional_execution_guardrail("llm-conditional", 1, Arc::new(|_| Ok(None))) - .unwrap(); ctx.register_llm_execution_intercept( "llm-exec", 1, @@ -1954,8 +2020,13 @@ fn test_plugin_registration_context_maps_deregistration_errors() { }), ) .unwrap(); - ctx.register_tool_request_intercept("tool-request", 1, false, Arc::new(|_name, args| Ok(args))) - .unwrap(); + ctx.register_tool_request_intercept( + "tool-request", + 1, + false, + Arc::new(|_name, args| Box::pin(async move { Ok(args) })), + ) + .unwrap(); ctx.register_tool_execution_intercept( "tool-exec", 1, diff --git a/crates/core/tests/unit/shared_tests.rs b/crates/core/tests/unit/shared_tests.rs index 9d17ce188..1de4a9ed7 100644 --- a/crates/core/tests/unit/shared_tests.rs +++ b/crates/core/tests/unit/shared_tests.rs @@ -168,8 +168,9 @@ fn stale_process_runtime_owner_is_reclaimed() { reset_global(); } -#[test] -fn test_run_request_intercepts_with_codec_none_and_codec_paths() { +#[tokio::test] +#[allow(clippy::await_holding_lock)] // Serializes access to global runtime state. +async fn test_run_request_intercepts_with_codec_none_and_codec_paths() { let _guard = lock_runtime_owner(); reset_global(); @@ -178,11 +179,13 @@ fn test_run_request_intercepts_with_codec_none_and_codec_paths() { 1, false, Arc::new(|_name, mut request, annotated| { - assert!(annotated.is_none()); - request.headers.insert("x-no-codec".into(), json!(true)); - let mut annotated = SharedTestCodec.decode(&request)?; - annotated.model = Some("interceptor-model".into()); - Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + Box::pin(async move { + assert!(annotated.is_none()); + request.headers.insert("x-no-codec".into(), json!(true)); + let mut annotated = SharedTestCodec.decode(&request)?; + annotated.model = Some("interceptor-model".into()); + Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + }) }), ) .unwrap(); @@ -196,6 +199,7 @@ fn test_run_request_intercepts_with_codec_none_and_codec_paths() { }, None, ) + .await .unwrap(); assert_eq!( request_without_codec.headers.get("x-no-codec"), @@ -215,10 +219,12 @@ fn test_run_request_intercepts_with_codec_none_and_codec_paths() { 1, false, Arc::new(|_name, mut request, annotated| { - let mut annotated = annotated.expect("codec should provide annotated request"); - annotated.model = Some("intercepted-model".into()); - request.headers.insert("x-codec".into(), json!(true)); - Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + Box::pin(async move { + let mut annotated = annotated.expect("codec should provide annotated request"); + annotated.model = Some("intercepted-model".into()); + request.headers.insert("x-codec".into(), json!(true)); + Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) + }) }), ) .unwrap(); @@ -233,6 +239,7 @@ fn test_run_request_intercepts_with_codec_none_and_codec_paths() { }, Some(codec), ) + .await .unwrap(); assert_eq!( @@ -259,8 +266,9 @@ fn test_run_request_intercepts_with_codec_none_and_codec_paths() { reset_global(); } -#[test] -fn managed_request_chain_records_contributions_incrementally_while_standalone_retains_them() { +#[tokio::test] +#[allow(clippy::await_holding_lock)] // Serializes access to global runtime state. +async fn managed_request_chain_records_contributions_incrementally_while_standalone_retains_them() { let _guard = lock_runtime_owner(); reset_global(); @@ -269,11 +277,12 @@ fn managed_request_chain_records_contributions_incrementally_while_standalone_re 1, false, Arc::new(|_name, request, annotated| { - Ok( - LlmRequestInterceptOutcome::new(request, annotated).with_optimization_contribution( - LlmOptimizationContribution::new("accepted", "custom"), - ), - ) + Box::pin(async move { + Ok(LlmRequestInterceptOutcome::new(request, annotated) + .with_optimization_contribution(LlmOptimizationContribution::new( + "accepted", "custom", + ))) + }) }), ) .unwrap(); @@ -282,14 +291,13 @@ fn managed_request_chain_records_contributions_incrementally_while_standalone_re 2, false, Arc::new(|_name, request, annotated| { - Ok( - LlmRequestInterceptOutcome::new(request, annotated).with_optimization_contribution( - LlmOptimizationContribution::new( + Box::pin(async move { + Ok(LlmRequestInterceptOutcome::new(request, annotated) + .with_optimization_contribution(LlmOptimizationContribution::new( "x".repeat(MAX_LLM_OPTIMIZATION_CONTRIBUTION_BYTES), "custom", - ), - ), - ) + ))) + }) }), ) .unwrap(); @@ -302,6 +310,7 @@ fn managed_request_chain_records_contributions_incrementally_while_standalone_re }, None, ) + .await .unwrap(); assert_eq!(standalone.3.len(), 2); assert!(standalone.3.iter().all(|item| item.sequence.is_none())); @@ -316,6 +325,7 @@ fn managed_request_chain_records_contributions_incrementally_while_standalone_re None, &recorder, ) + .await .unwrap(); assert!(managed.3.is_empty()); let recorded = recorder.unemitted(); @@ -326,8 +336,9 @@ fn managed_request_chain_records_contributions_incrementally_while_standalone_re reset_global(); } -#[test] -fn test_run_request_intercepts_injects_dynamo_agent_lineage() { +#[tokio::test] +#[allow(clippy::await_holding_lock)] // Serializes access to global runtime state. +async fn test_run_request_intercepts_injects_dynamo_agent_lineage() { let _guard = lock_runtime_owner(); reset_global(); @@ -369,6 +380,7 @@ fn test_run_request_intercepts_injects_dynamo_agent_lineage() { }, None, ) + .await .unwrap(); assert_eq!( request.headers.get(DYNAMO_SESSION_ID_HEADER_KEY), @@ -396,6 +408,7 @@ fn test_run_request_intercepts_injects_dynamo_agent_lineage() { }, Some(Arc::new(SharedTestCodec)), ) + .await .unwrap(); assert_eq!( request_with_codec.headers.get(DYNAMO_SESSION_ID_HEADER_KEY), @@ -433,6 +446,7 @@ fn test_run_request_intercepts_injects_dynamo_agent_lineage() { }, None, ) + .await .unwrap(); assert_eq!( request.headers.get(DYNAMO_SESSION_ID_HEADER_KEY), @@ -475,6 +489,7 @@ fn test_run_request_intercepts_injects_dynamo_agent_lineage() { }, None, ) + .await .unwrap(); assert!(!request.headers.contains_key(DYNAMO_SESSION_ID_HEADER_KEY)); assert!( diff --git a/crates/ffi/src/api/mod.rs b/crates/ffi/src/api/mod.rs index 03103f58a..f2984f32e 100644 --- a/crates/ffi/src/api/mod.rs +++ b/crates/ffi/src/api/mod.rs @@ -99,6 +99,15 @@ fn tokio_runtime() -> &'static Runtime { }) } +fn block_on_sync_ffi(future: impl Future>) -> FlowResult { + if tokio::runtime::Handle::try_current().is_ok() { + return Err(nemo_relay::error::FlowError::Internal( + "synchronous FFI middleware helpers cannot run on a Tokio runtime thread; use the completion-based async registration API".into(), + )); + } + tokio_runtime().block_on(future) +} + // --------------------------------------------------------------------------- // Standalone middleware chains // --------------------------------------------------------------------------- @@ -141,7 +150,7 @@ pub unsafe extern "C" fn nemo_relay_tool_request_intercepts( Some(a) => a, None => return NemoRelayStatus::InvalidJson, }; - match core_tool_api::tool_request_intercepts(&name, args) { + match block_on_sync_ffi(core_tool_api::tool_request_intercepts(&name, args)) { Ok(result) => { unsafe { *out = json_to_c_string(&result) }; NemoRelayStatus::Ok @@ -179,7 +188,7 @@ pub unsafe extern "C" fn nemo_relay_tool_conditional_execution( Some(a) => a, None => return NemoRelayStatus::InvalidJson, }; - match core_tool_api::tool_conditional_execution(&name, &args) { + match block_on_sync_ffi(core_tool_api::tool_conditional_execution(&name, &args)) { Ok(()) => NemoRelayStatus::Ok, Err(e) => status_from_error(&e), } @@ -233,7 +242,7 @@ pub unsafe extern "C" fn nemo_relay_llm_request_intercepts( return NemoRelayStatus::InvalidJson; } }; - match core_llm_api::llm_request_intercepts(name_str, request) { + match block_on_sync_ffi(core_llm_api::llm_request_intercepts(name_str, request)) { Ok(transformed) => { let result_json = serde_json::to_value(&transformed).unwrap_or(serde_json::Value::Null); unsafe { *out = json_to_c_string(&result_json) }; @@ -396,7 +405,7 @@ pub unsafe extern "C" fn nemo_relay_llm_conditional_execution( return NemoRelayStatus::InvalidJson; } }; - match core_llm_api::llm_conditional_execution(&request) { + match block_on_sync_ffi(core_llm_api::llm_conditional_execution(&request)) { Ok(()) => NemoRelayStatus::Ok, Err(e) => status_from_error(&e), } diff --git a/crates/ffi/src/callable.rs b/crates/ffi/src/callable.rs index f327e01d2..f1c952863 100644 --- a/crates/ffi/src/callable.rs +++ b/crates/ffi/src/callable.rs @@ -321,14 +321,17 @@ pub fn wrap_tool_sanitize_fn( free_fn: NemoRelayFreeFn, ) -> ToolSanitizeFn { let ud = make_user_data(user_data, free_fn); - Arc::new(move |name: &str, args: Json| { - let c_name = CString::new(name).unwrap_or_default(); - let c_args = json_to_c_string(&args); - let result_ptr = unsafe { cb(ud.ptr, c_name.as_ptr(), c_args) }; - unsafe { nemo_relay_string_free_internal(c_args) }; - let result = ptr_to_json(result_ptr); - unsafe { nemo_relay_string_free_internal(result_ptr) }; - result + Arc::new(move |name: String, args: Json| { + let ud = ud.clone(); + Box::pin(async move { + let c_name = CString::new(name).unwrap_or_default(); + let c_args = json_to_c_string(&args); + let result_ptr = unsafe { cb(ud.ptr, c_name.as_ptr(), c_args) }; + unsafe { nemo_relay_string_free_internal(c_args) }; + let result = ptr_to_json(result_ptr); + unsafe { nemo_relay_string_free_internal(result_ptr) }; + Ok(result) + }) }) } @@ -339,22 +342,25 @@ pub fn wrap_tool_conditional_fn( free_fn: NemoRelayFreeFn, ) -> ToolConditionalFn { let ud = make_user_data(user_data, free_fn); - Arc::new(move |name: &str, args: &Json| { - clear_last_error(); - let c_name = CString::new(name).unwrap_or_default(); - let c_args = json_to_c_string(args); - let result_ptr = unsafe { cb(ud.ptr, c_name.as_ptr(), c_args) }; - unsafe { nemo_relay_string_free_internal(c_args) }; - let result = if result_ptr.is_null() { - match last_error_message() { - Some(message) => Err(FlowError::Internal(message)), - None => Ok(None), - } - } else { - Ok(ptr_to_opt_string(result_ptr)) - }; - unsafe { nemo_relay_string_free_internal(result_ptr) }; - result + Arc::new(move |name: String, args: Json| { + let ud = ud.clone(); + Box::pin(async move { + clear_last_error(); + let c_name = CString::new(name).unwrap_or_default(); + let c_args = json_to_c_string(&args); + let result_ptr = unsafe { cb(ud.ptr, c_name.as_ptr(), c_args) }; + unsafe { nemo_relay_string_free_internal(c_args) }; + let result = if result_ptr.is_null() { + match last_error_message() { + Some(message) => Err(FlowError::Internal(message)), + None => Ok(None), + } + } else { + Ok(ptr_to_opt_string(result_ptr)) + }; + unsafe { nemo_relay_string_free_internal(result_ptr) }; + result + }) }) } @@ -365,16 +371,19 @@ pub fn wrap_tool_request_intercept_fn( free_fn: NemoRelayFreeFn, ) -> ToolInterceptFn { let ud = make_user_data(user_data, free_fn); - Arc::new(move |name: &str, args: Json| { - clear_last_error(); - let c_name = CString::new(name).unwrap_or_default(); - let c_args = json_to_c_string(&args); - let result_ptr = unsafe { cb(ud.ptr, c_name.as_ptr(), c_args) }; - unsafe { nemo_relay_string_free_internal(c_args) }; - let result = - json_result_from_ptr(result_ptr, "tool request intercept callback returned null"); - unsafe { nemo_relay_string_free_internal(result_ptr) }; - result + Arc::new(move |name: String, args: Json| { + let ud = ud.clone(); + Box::pin(async move { + clear_last_error(); + let c_name = CString::new(name).unwrap_or_default(); + let c_args = json_to_c_string(&args); + let result_ptr = unsafe { cb(ud.ptr, c_name.as_ptr(), c_args) }; + unsafe { nemo_relay_string_free_internal(c_args) }; + let result = + json_result_from_ptr(result_ptr, "tool request intercept callback returned null"); + unsafe { nemo_relay_string_free_internal(result_ptr) }; + result + }) }) } @@ -617,64 +626,67 @@ pub fn wrap_llm_request_intercept_fn( ) -> LlmRequestInterceptFn { let ud = make_user_data(user_data, free_fn); Arc::new( - move |name: &str, request: LlmRequest, annotated: Option| { - clear_last_error(); - let c_name = CString::new(name).unwrap_or_default(); - let ffi_req = Box::into_raw(Box::new(FfiLLMRequest(request))); + move |name: String, request: LlmRequest, annotated: Option| { + let ud = ud.clone(); + Box::pin(async move { + clear_last_error(); + let c_name = CString::new(name).unwrap_or_default(); + let ffi_req = Box::into_raw(Box::new(FfiLLMRequest(request))); + + // Serialize annotated to JSON C string if present, else null + let c_annotated = match &annotated { + Some(a) => { + let s = serde_json::to_string(a).unwrap_or_else(|_| "null".to_string()); + CString::new(s).unwrap_or_default() + } + None => CString::default(), + }; + let annotated_ptr = if annotated.is_some() { + c_annotated.as_ptr() + } else { + std::ptr::null() + }; - // Serialize annotated to JSON C string if present, else null - let c_annotated = match &annotated { - Some(a) => { - let s = serde_json::to_string(a).unwrap_or_else(|_| "null".to_string()); - CString::new(s).unwrap_or_default() - } - None => CString::default(), - }; - let annotated_ptr = if annotated.is_some() { - c_annotated.as_ptr() - } else { - std::ptr::null() - }; + let mut out_outcome: *mut c_char = std::ptr::null_mut(); - let mut out_outcome: *mut c_char = std::ptr::null_mut(); + let status = unsafe { + cb( + ud.ptr, + c_name.as_ptr(), + ffi_req, + annotated_ptr, + &mut out_outcome, + ) + }; - let status = unsafe { - cb( - ud.ptr, - c_name.as_ptr(), - ffi_req, - annotated_ptr, - &mut out_outcome, - ) - }; + // Free the input request + unsafe { drop(Box::from_raw(ffi_req)) }; - // Free the input request - unsafe { drop(Box::from_raw(ffi_req)) }; + if status != NemoRelayStatus::Ok { + unsafe { nemo_relay_string_free_internal(out_outcome) }; + let message = last_error_message() + .unwrap_or_else(|| "request intercept callback failed".to_string()); + return Err(FlowError::Internal(message)); + } - if status != NemoRelayStatus::Ok { + if out_outcome.is_null() { + return Err(FlowError::Internal( + "request intercept returned null out_outcome_json".to_string(), + )); + } + let outcome = unsafe { CStr::from_ptr(out_outcome) } + .to_str() + .map_err(|error| FlowError::Internal(format!("invalid outcome UTF-8: {error}"))) + .and_then(|json| { + serde_json::from_str::(json).map_err(|error| { + FlowError::Internal(format!( + "invalid LLM request intercept outcome JSON: {error}" + )) + }) + }); unsafe { nemo_relay_string_free_internal(out_outcome) }; - let message = last_error_message() - .unwrap_or_else(|| "request intercept callback failed".to_string()); - return Err(FlowError::Internal(message)); - } - - if out_outcome.is_null() { - return Err(FlowError::Internal( - "request intercept returned null out_outcome_json".to_string(), - )); - } - let outcome = unsafe { CStr::from_ptr(out_outcome) } - .to_str() - .map_err(|error| FlowError::Internal(format!("invalid outcome UTF-8: {error}"))) - .and_then(|json| { - serde_json::from_str::(json).map_err(|error| { - FlowError::Internal(format!( - "invalid LLM request intercept outcome JSON: {error}" - )) - }) - }); - unsafe { nemo_relay_string_free_internal(out_outcome) }; - outcome + outcome + }) }, ) } @@ -688,79 +700,85 @@ pub fn wrap_llm_sanitize_request_fn( let ud = make_user_data(user_data, free_fn); Arc::new( move |request: LlmRequest, context: LlmSanitizeRequestContext| { + let ud = ud.clone(); + Box::pin(async move { + clear_last_error(); + let (codec_kind, codec_id) = match ffi_codec_identity(context.codec()) { + Ok(identity) => identity, + Err(error) => { + set_last_error(&error.to_string()); + return Ok(None); + } + }; + let codec = context + .resolve_codec() + .map(crate::types::FfiLlmSanitizeRequestCodec); + let ffi_context = NemoRelayLlmSanitizeRequestContext { + codec_kind, + codec_id: codec_id + .as_ref() + .map_or(std::ptr::null(), |name| name.as_ptr()), + codec: codec.as_ref().map_or(std::ptr::null(), std::ptr::from_ref), + }; + let ffi_req = Box::into_raw(Box::new(FfiLLMRequest(request))); + let result_ptr = unsafe { cb(ud.ptr, ffi_req, ffi_context) }; + if result_ptr.is_null() { + unsafe { drop(Box::from_raw(ffi_req)) }; + return Ok(None); + } + if result_ptr == ffi_req { + return Ok(Some(unsafe { Box::from_raw(ffi_req) }.0)); + } + unsafe { drop(Box::from_raw(ffi_req)) }; + Ok(Some(unsafe { Box::from_raw(result_ptr) }.0)) + }) + }, + ) +} + +/// Wrap a C LLM response sanitizer into a Rust closure. +pub fn wrap_llm_sanitize_response_fn( + cb: NemoRelayLlmSanitizeResponseCb, + user_data: *mut libc::c_void, + free_fn: NemoRelayFreeFn, +) -> LlmSanitizeResponseFn { + let ud = make_user_data(user_data, free_fn); + Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { + let ud = ud.clone(); + Box::pin(async move { clear_last_error(); let (codec_kind, codec_id) = match ffi_codec_identity(context.codec()) { Ok(identity) => identity, Err(error) => { set_last_error(&error.to_string()); - return None; + return Ok(None); } }; let codec = context .resolve_codec() - .map(crate::types::FfiLlmSanitizeRequestCodec); - let ffi_context = NemoRelayLlmSanitizeRequestContext { + .map(crate::types::FfiLlmSanitizeResponseCodec); + let ffi_context = NemoRelayLlmSanitizeResponseContext { codec_kind, codec_id: codec_id .as_ref() .map_or(std::ptr::null(), |name| name.as_ptr()), codec: codec.as_ref().map_or(std::ptr::null(), std::ptr::from_ref), }; - let ffi_req = Box::into_raw(Box::new(FfiLLMRequest(request))); - let result_ptr = unsafe { cb(ud.ptr, ffi_req, ffi_context) }; + let response_json = json_to_c_string(&response); + let result_ptr = unsafe { cb(ud.ptr, response_json, ffi_context) }; if result_ptr.is_null() { - unsafe { drop(Box::from_raw(ffi_req)) }; - return None; - } - if result_ptr == ffi_req { - return Some(unsafe { Box::from_raw(ffi_req) }.0); + unsafe { nemo_relay_string_free_internal(response_json) }; + return Ok(None); } - unsafe { drop(Box::from_raw(ffi_req)) }; - Some(unsafe { Box::from_raw(result_ptr) }.0) - }, - ) -} - -/// Wrap a C LLM response sanitizer into a Rust closure. -pub fn wrap_llm_sanitize_response_fn( - cb: NemoRelayLlmSanitizeResponseCb, - user_data: *mut libc::c_void, - free_fn: NemoRelayFreeFn, -) -> LlmSanitizeResponseFn { - let ud = make_user_data(user_data, free_fn); - Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { - clear_last_error(); - let (codec_kind, codec_id) = match ffi_codec_identity(context.codec()) { - Ok(identity) => identity, - Err(error) => { - set_last_error(&error.to_string()); - return None; - } - }; - let codec = context - .resolve_codec() - .map(crate::types::FfiLlmSanitizeResponseCodec); - let ffi_context = NemoRelayLlmSanitizeResponseContext { - codec_kind, - codec_id: codec_id - .as_ref() - .map_or(std::ptr::null(), |name| name.as_ptr()), - codec: codec.as_ref().map_or(std::ptr::null(), std::ptr::from_ref), - }; - let response_json = json_to_c_string(&response); - let result_ptr = unsafe { cb(ud.ptr, response_json, ffi_context) }; - if result_ptr.is_null() { - unsafe { nemo_relay_string_free_internal(response_json) }; - return None; - } - let result = c_str_to_json(result_ptr); - unsafe { - nemo_relay_string_free_internal(response_json); - if result_ptr != response_json { - nemo_relay_string_free_internal(result_ptr); + let result = c_str_to_json(result_ptr); + unsafe { + nemo_relay_string_free_internal(response_json); + if result_ptr != response_json { + nemo_relay_string_free_internal(result_ptr); + } } - } - result + Ok(result) + }) }) } @@ -790,20 +808,23 @@ pub fn wrap_llm_conditional_fn( free_fn: NemoRelayFreeFn, ) -> LlmConditionalFn { let ud = make_user_data(user_data, free_fn); - Arc::new(move |request: &LlmRequest| { - clear_last_error(); - let ffi_req = FfiLLMRequest(request.clone()); - let result_ptr = unsafe { cb(ud.ptr, &ffi_req) }; - let result = if result_ptr.is_null() { - match last_error_message() { - Some(message) => Err(FlowError::Internal(message)), - None => Ok(None), - } - } else { - Ok(ptr_to_opt_string(result_ptr)) - }; - unsafe { nemo_relay_string_free_internal(result_ptr) }; - result + Arc::new(move |request: LlmRequest| { + let ud = ud.clone(); + Box::pin(async move { + clear_last_error(); + let ffi_req = FfiLLMRequest(request); + let result_ptr = unsafe { cb(ud.ptr, &ffi_req) }; + let result = if result_ptr.is_null() { + match last_error_message() { + Some(message) => Err(FlowError::Internal(message)), + None => Ok(None), + } + } else { + Ok(ptr_to_opt_string(result_ptr)) + }; + unsafe { nemo_relay_string_free_internal(result_ptr) }; + result + }) }) } @@ -918,14 +939,18 @@ pub fn wrap_event_sanitize_fn( free_fn: NemoRelayFreeFn, ) -> EventSanitizeFn { let ud = make_user_data(user_data, free_fn); - Arc::new(move |event: &Event, fields: EventSanitizeFields| { - let ffi_event = FfiEvent(event.clone()); - let fields_json = json_to_c_string(&serde_json::to_value(&fields).unwrap_or(Json::Null)); - let result_ptr = unsafe { cb(ud.ptr, &ffi_event, fields_json) }; - unsafe { nemo_relay_string_free_internal(fields_json) }; - let result = serde_json::from_value(ptr_to_json(result_ptr)).unwrap_or_default(); - unsafe { nemo_relay_string_free_internal(result_ptr) }; - result + Arc::new(move |event: Event, fields: EventSanitizeFields| { + let ud = ud.clone(); + Box::pin(async move { + let ffi_event = FfiEvent(event); + let fields_json = + json_to_c_string(&serde_json::to_value(&fields).unwrap_or(Json::Null)); + let result_ptr = unsafe { cb(ud.ptr, &ffi_event, fields_json) }; + unsafe { nemo_relay_string_free_internal(fields_json) }; + let result = serde_json::from_value(ptr_to_json(result_ptr)).unwrap_or_default(); + unsafe { nemo_relay_string_free_internal(result_ptr) }; + Ok(result) + }) }) } diff --git a/crates/ffi/tests/integration/callable_extra_tests.rs b/crates/ffi/tests/integration/callable_extra_tests.rs index b176f4988..69a341b96 100644 --- a/crates/ffi/tests/integration/callable_extra_tests.rs +++ b/crates/ffi/tests/integration/callable_extra_tests.rs @@ -4,10 +4,19 @@ //! Integration tests for callable extra in the NeMo Relay FFI crate. use super::*; +use std::future::Future; use std::ptr; use tokio_stream::StreamExt; +fn resolve(future: impl Future) -> T { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(future) +} + unsafe extern "C" fn tool_conditional_error_cb( _user_data: *mut libc::c_void, _name: *const c_char, @@ -172,7 +181,7 @@ fn test_callable_extra_trampoline_and_helper_paths() { .unwrap(); let conditional = wrap_tool_conditional_fn(tool_conditional_error_cb, ptr::null_mut(), None); - let conditional_err = conditional("tool", &json!({})).unwrap_err(); + let conditional_err = resolve(conditional("tool".into(), json!({}))).unwrap_err(); assert!( conditional_err .to_string() @@ -243,7 +252,7 @@ fn test_callable_extra_request_intercept_and_codec_paths() { let intercept_error = wrap_llm_request_intercept_fn(llm_request_intercept_status_error_cb, ptr::null_mut(), None); - let err = intercept_error("llm", request.clone(), None).unwrap_err(); + let err = resolve(intercept_error("llm".into(), request.clone(), None)).unwrap_err(); assert!( err.to_string() .contains("request intercept callback failed") @@ -254,7 +263,7 @@ fn test_callable_extra_request_intercept_and_codec_paths() { ptr::null_mut(), None, ); - let err = intercept_null("llm", request.clone(), None).unwrap_err(); + let err = resolve(intercept_null("llm".into(), request.clone(), None)).unwrap_err(); assert!(err.to_string().contains("null out_outcome_json")); let intercept_invalid_annotated = wrap_llm_request_intercept_fn( @@ -262,22 +271,28 @@ fn test_callable_extra_request_intercept_and_codec_paths() { ptr::null_mut(), None, ); - let err = intercept_invalid_annotated("llm", request.clone(), None).unwrap_err(); + let err = resolve(intercept_invalid_annotated( + "llm".into(), + request.clone(), + None, + )) + .unwrap_err(); assert!( err.to_string() .contains("invalid LLM request intercept outcome JSON") ); let sanitize = wrap_llm_sanitize_request_fn(llm_request_passthrough_cb, ptr::null_mut(), None); - let sanitized = sanitize( + let sanitized = resolve(sanitize( request.clone(), nemo_relay::api::runtime::LlmSanitizeRequestContext::default(), - ) + )) + .unwrap() .expect("non-null sanitizer result"); assert_eq!(sanitized.content, request.content); let conditional = wrap_llm_conditional_fn(llm_conditional_error_cb, ptr::null_mut(), None); - let conditional_err = conditional(&request).unwrap_err(); + let conditional_err = resolve(conditional(request.clone())).unwrap_err(); assert!( conditional_err .to_string() @@ -359,12 +374,16 @@ fn test_sanitizer_context_resolves_directional_ffi_codecs() { "preserve": true }), }; - let sanitized = - wrap_llm_sanitize_request_fn(llm_request_codec_round_trip_cb, ptr::null_mut(), None)( - request.clone(), - LlmSanitizeRequestContext::for_request_codec(Some(codec.clone())), - ) - .expect("codec round trip returns a request"); + let sanitized = resolve(wrap_llm_sanitize_request_fn( + llm_request_codec_round_trip_cb, + ptr::null_mut(), + None, + )( + request.clone(), + LlmSanitizeRequestContext::for_request_codec(Some(codec.clone())), + )) + .unwrap() + .expect("codec round trip returns a request"); assert_eq!(sanitized.content, request.content); let response = json!({ @@ -376,11 +395,15 @@ fn test_sanitizer_context_resolves_directional_ffi_codecs() { "finish_reason": "stop" }] }); - let sanitized = - wrap_llm_sanitize_response_fn(llm_response_codec_decode_cb, ptr::null_mut(), None)( - response.clone(), - LlmSanitizeResponseContext::for_response_codec(Some(codec)), - ) - .expect("codec decode returns a response"); + let sanitized = resolve(wrap_llm_sanitize_response_fn( + llm_response_codec_decode_cb, + ptr::null_mut(), + None, + )( + response.clone(), + LlmSanitizeResponseContext::for_response_codec(Some(codec)), + )) + .unwrap() + .expect("codec decode returns a response"); assert_eq!(sanitized, response); } diff --git a/crates/ffi/tests/unit/callable_tests.rs b/crates/ffi/tests/unit/callable_tests.rs index c58098b4a..60b260868 100644 --- a/crates/ffi/tests/unit/callable_tests.rs +++ b/crates/ffi/tests/unit/callable_tests.rs @@ -4,6 +4,7 @@ //! Unit tests for callable in the NeMo Relay FFI crate. use super::*; +use std::future::Future; use std::sync::atomic::{AtomicUsize, Ordering}; use nemo_relay::api::event::{Event, EventSanitizeFields}; @@ -22,6 +23,14 @@ fn user_data_counter() -> (*mut libc::c_void, Arc) { (ptr, counter) } +fn resolve(future: impl Future) -> T { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(future) +} + unsafe extern "C" fn tool_sanitize_cb( user_data: *mut libc::c_void, name: *const c_char, @@ -328,7 +337,7 @@ fn make_request() -> LlmRequest { fn test_wrap_tool_request_and_conditional_callbacks() { let (user_data, called) = user_data_counter(); let wrapped = wrap_tool_sanitize_fn(tool_sanitize_cb, user_data, Some(free_arc_counter)); - let result = wrapped("tool-name", json!({"value": 1})); + let result = resolve(wrapped("tool-name".into(), json!({"value": 1}))).unwrap(); assert_eq!(result["value"], json!(1)); assert_eq!(result["name"], json!("tool-name")); assert_eq!(called.load(Ordering::SeqCst), 1); @@ -338,11 +347,11 @@ fn test_wrap_tool_request_and_conditional_callbacks() { let wrapped_conditional = wrap_tool_conditional_fn(tool_conditional_cb, std::ptr::null_mut(), None); assert_eq!( - wrapped_conditional("tool", &json!({"block": true})).unwrap(), + resolve(wrapped_conditional("tool".into(), json!({"block": true}))).unwrap(), Some("blocked".into()) ); assert_eq!( - wrapped_conditional("tool", &json!({"block": false})).unwrap(), + resolve(wrapped_conditional("tool".into(), json!({"block": false}))).unwrap(), None ); } @@ -411,56 +420,60 @@ fn test_wrap_tool_exec_and_intercept_callbacks() { fn test_wrap_llm_request_response_and_conditional_callbacks() { let request_intercept = wrap_llm_request_intercept_fn(llm_request_intercept_cb, std::ptr::null_mut(), None); - let outcome = request_intercept("llm", make_request(), None).unwrap(); + let outcome = resolve(request_intercept("llm".into(), make_request(), None)).unwrap(); assert_eq!(outcome.request.content["intercepted"], json!(true)); let sanitize_request = wrap_llm_sanitize_request_fn(llm_request_null_cb, std::ptr::null_mut(), None); assert_eq!( - sanitize_request( + resolve(sanitize_request( make_request(), nemo_relay::api::runtime::LlmSanitizeRequestContext::default(), - ), + )) + .unwrap(), None ); let alias_request = wrap_llm_sanitize_request_fn(llm_request_alias_cb, std::ptr::null_mut(), None); assert_eq!( - alias_request( + resolve(alias_request( make_request(), nemo_relay::api::runtime::LlmSanitizeRequestContext::default(), - ), + )) + .unwrap(), Some(make_request()) ); let conditional = wrap_llm_conditional_fn(llm_conditional_cb, std::ptr::null_mut(), None); assert_eq!( - conditional(&LlmRequest { + resolve(conditional(LlmRequest { headers: serde_json::Map::new(), content: json!({"block": true}), - }) + })) .unwrap(), Some("blocked llm".into()) ); - assert_eq!(conditional(&make_request()).unwrap(), None); + assert_eq!(resolve(conditional(make_request())).unwrap(), None); let wrapped_response = wrap_llm_sanitize_response_fn(json_cb, std::ptr::null_mut(), None); assert_eq!( - wrapped_response( + resolve(wrapped_response( json!({"value": 2}), nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), - ) + )) + .unwrap() .unwrap()["wrapped"], json!(true) ); let alias_response = wrap_llm_sanitize_response_fn(json_alias_cb, std::ptr::null_mut(), None); assert_eq!( - alias_response( + resolve(alias_response( json!({"value": 2}), nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), - ), + )) + .unwrap(), Some(json!({"value": 2})) ); @@ -468,10 +481,11 @@ fn test_wrap_llm_request_response_and_conditional_callbacks() { let malformed_response = wrap_llm_sanitize_response_fn(callback, std::ptr::null_mut(), None); assert_eq!( - malformed_response( + resolve(malformed_response( json!({"secret": "must be omitted"}), nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), - ), + )) + .unwrap(), None ); } @@ -484,14 +498,17 @@ fn test_llm_sanitizers_fail_closed_for_runtime_codec_ids_with_embedded_nul() { let request_sanitizer = wrap_llm_sanitize_request_fn(llm_request_alias_cb, std::ptr::null_mut(), None); - assert_eq!( - request_sanitizer( - make_request(), - nemo_relay::api::runtime::LlmSanitizeRequestContext::with_identity( - runtime_identity.clone(), - ), + let request_error = resolve(request_sanitizer( + make_request(), + nemo_relay::api::runtime::LlmSanitizeRequestContext::with_identity( + runtime_identity.clone(), ), - None + )) + .expect_err("an embedded runtime codec ID must fail the callback wrapper"); + assert!( + request_error + .to_string() + .contains("runtime codec ID contains an embedded NUL") ); assert!( last_error_message() @@ -501,12 +518,15 @@ fn test_llm_sanitizers_fail_closed_for_runtime_codec_ids_with_embedded_nul() { let response_sanitizer = wrap_llm_sanitize_response_fn(json_alias_cb, std::ptr::null_mut(), None); - assert_eq!( - response_sanitizer( - json!({"secret": "must be omitted"}), - nemo_relay::api::runtime::LlmSanitizeResponseContext::with_identity(runtime_identity), - ), - None + let response_error = resolve(response_sanitizer( + json!({"secret": "must be omitted"}), + nemo_relay::api::runtime::LlmSanitizeResponseContext::with_identity(runtime_identity), + )) + .expect_err("an embedded runtime codec ID must fail the callback wrapper"); + assert!( + response_error + .to_string() + .contains("runtime codec ID contains an embedded NUL") ); assert!( last_error_message() @@ -542,7 +562,12 @@ fn test_wrap_llm_request_intercept_with_annotated_input() { stream: None, extra: serde_json::Map::from_iter([("annotated".into(), json!(true))]), }; - let outcome = request_intercept("llm", make_request(), Some(annotated)).unwrap(); + let outcome = resolve(request_intercept( + "llm".into(), + make_request(), + Some(annotated), + )) + .unwrap(); assert_eq!(outcome.request.content["intercepted"], json!(true)); let annotated_out = outcome .annotated_request @@ -651,7 +676,7 @@ fn test_wrap_llm_exec_stream_and_event_callbacks() { .build(); let (user_data, sanitize_calls) = user_data_counter(); let sanitizer = wrap_event_sanitize_fn(event_sanitize_cb, user_data, Some(free_arc_counter)); - let sanitized = sanitizer(&event, original_fields.clone()); + let sanitized = resolve(sanitizer(event.clone(), original_fields.clone())).unwrap(); assert_eq!(sanitized.data, Some(json!({"safe": true}))); assert_eq!( sanitized @@ -667,12 +692,12 @@ fn test_wrap_llm_exec_stream_and_event_callbacks() { let invalid = wrap_event_sanitize_fn(invalid_event_sanitize_cb, std::ptr::null_mut(), None); assert_eq!( - invalid(&event, original_fields.clone()), + resolve(invalid(event.clone(), original_fields.clone())).unwrap(), EventSanitizeFields::default() ); let null = wrap_event_sanitize_fn(null_event_sanitize_cb, std::ptr::null_mut(), None); assert_eq!( - null(&event, original_fields.clone()), + resolve(null(event, original_fields.clone())).unwrap(), EventSanitizeFields::default() ); diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 24df871fb..2efa65892 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -79,11 +79,9 @@ use crate::convert::{ get_last_callback_error as get_recorded_callback_error, opt_json, parse_timestamp_micros, record_callback_error, to_napi_err, }; +use crate::promise_call::PromiseAwareFn; use crate::stream::LlmStream; -use crate::types::{ - EventSanitizeFields, LlmHandle, ScopeHandle, ScopeStack, ScopeType, ToolHandle, - event_sanitize_fields_from_json, -}; +use crate::types::{LlmHandle, ScopeHandle, ScopeStack, ScopeType, ToolHandle}; #[napi::module_init] fn init() { @@ -756,7 +754,9 @@ fn build_plugin_context( core_registry_api::register_tool_sanitize_request_guardrail( &name, priority, - callable::wrap_js_tool_fn(middleware_tool_callback_tsfn(ctx.env, &callback)?), + callable::wrap_js_tool_sanitize_promise_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -800,7 +800,9 @@ fn build_plugin_context( core_registry_api::register_tool_sanitize_response_guardrail( &name, priority, - callable::wrap_js_tool_fn(middleware_tool_callback_tsfn(ctx.env, &callback)?), + callable::wrap_js_tool_sanitize_promise_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -840,9 +842,9 @@ fn build_plugin_context( core_registry_api::register_tool_conditional_execution_guardrail( &name, priority, - callable::wrap_js_tool_conditional_fn(middleware_tool_callback_tsfn( + callable::wrap_js_tool_conditional_promise_fn(Arc::new(PromiseAwareFn::new( ctx.env, &callback, - )?), + )?)), ) .map_err(to_napi_err)?; @@ -888,9 +890,9 @@ fn build_plugin_context( core_registry_api::register_llm_sanitize_request_guardrail( &name, priority, - callable::wrap_js_llm_sanitize_request_fn( - middleware_llm_sanitize_request_callback_tsfn(ctx.env, &callback)?, - ), + callable::wrap_js_llm_sanitize_request_promise_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -934,9 +936,9 @@ fn build_plugin_context( core_registry_api::register_llm_sanitize_response_guardrail( &name, priority, - callable::wrap_js_llm_sanitize_response_fn( - middleware_llm_sanitize_response_callback_tsfn(ctx.env, &callback)?, - ), + callable::wrap_js_llm_sanitize_response_promise_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -976,9 +978,9 @@ fn build_plugin_context( core_registry_api::register_llm_conditional_execution_guardrail( &name, priority, - callable::wrap_js_llm_conditional_fn(middleware_json_callback_tsfn( + callable::wrap_js_llm_conditional_promise_fn(Arc::new(PromiseAwareFn::new( ctx.env, &callback, - )?), + )?)), ) .map_err(to_napi_err)?; @@ -1018,12 +1020,13 @@ fn build_plugin_context( let priority = ctx.get::(1)?; let break_chain = ctx.get::(2)?; let callback = ctx.get::(3)?; - let tsfn = middleware_json_callback_tsfn(ctx.env, &callback)?; core_registry_api::register_llm_request_intercept( &name, priority, break_chain, - callable::wrap_js_llm_request_intercept_fn(tsfn), + callable::wrap_js_llm_request_intercept_promise_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -1147,12 +1150,13 @@ fn build_plugin_context( let priority = ctx.get::(1)?; let break_chain = ctx.get::(2)?; let callback = ctx.get::(3)?; - let callback = middleware_tool_callback_tsfn(ctx.env, &callback)?; core_registry_api::register_tool_request_intercept( &name, priority, break_chain, - callable::wrap_js_tool_request_intercept_fn(callback), + callable::wrap_js_tool_request_intercept_promise_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -1384,40 +1388,6 @@ impl PersistentJsFunction { unsafe { Option::::from_napi_value(self.env, returned.raw()) }.map(callback_json) } - fn call_event_sanitize(&self, event: Json, fields: EventSanitizeFields) -> napi::Result { - let mut value = ptr::null_mut(); - // SAFETY: `self.reference` is a live N-API reference created in - // `self.env`, and `value` is writable storage for the borrowed - // function value. - let status = - unsafe { napi::sys::napi_get_reference_value(self.env, self.reference, &mut value) }; - if status != napi::sys::Status::napi_ok { - return Err(napi::Error::from_reason( - "failed to borrow event sanitizer function", - )); - } - // SAFETY: `value` was resolved from this struct's function reference, - // so it is a live function value in `self.env` for this call. - let func = unsafe { JsFunction::from_raw_unchecked(self.env, value) }; - // SAFETY: `Json::to_napi_value` created this event value in `self.env`, - // so wrapping it as `JsUnknown` is valid for the immediate callback. - let event = unsafe { - JsUnknown::from_raw_unchecked(self.env, Json::to_napi_value(self.env, event)?) - }; - // SAFETY: `EventSanitizeFields::to_napi_value` created this fields - // value in `self.env`, so wrapping it as `JsUnknown` is valid for the - // immediate callback. - let fields = unsafe { - JsUnknown::from_raw_unchecked( - self.env, - EventSanitizeFields::to_napi_value(self.env, fields)?, - ) - }; - let returned = func.call(None, &[event, fields])?; - // SAFETY: `returned` is the live result of invoking `func` in this environment. - unsafe { Option::::from_napi_value(self.env, returned.raw()) }.map(callback_json) - } - fn call_json(&self, argument: Json) -> napi::Result { let mut value = ptr::null_mut(); // SAFETY: `self.reference` is a live N-API reference created in @@ -1442,89 +1412,9 @@ impl PersistentJsFunction { } } -fn core_event_fields( - fields: EventSanitizeFields, -) -> Option { - Some(nemo_relay::api::event::EventSanitizeFields { - data: fields.data, - category_profile: fields - .category_profile - .map(serde_json::from_value) - .transpose() - .ok()?, - metadata: fields.metadata, - }) -} - -fn js_event_fields(fields: &nemo_relay::api::event::EventSanitizeFields) -> EventSanitizeFields { - EventSanitizeFields { - data: fields.data.clone(), - category_profile: fields - .category_profile - .as_ref() - .and_then(|value| serde_json::to_value(value).ok()), - metadata: fields.metadata.clone(), - } -} - fn node_event_sanitize_fn(env: &Env, func: &JsFunction) -> napi::Result { - let callback = callable::safe_middleware_callback(env, func)?; - let direct = Arc::new(PersistentJsFunction::new(env, &callback)?); - let register_thread = std::thread::current().id(); - let mut tsfn = callback.create_threadsafe_function( - 0, - |ctx: napi::threadsafe_function::ThreadSafeCallContext<(Json, Json)>| { - Ok(vec![ctx.value.0, ctx.value.1]) - }, - )?; - tsfn.unref(env)?; - let background = callable::wrap_js_event_sanitize_fn(tsfn); - Ok(Arc::new(move |event, fields| { - if std::thread::current().id() == register_thread { - let event_json = match event.try_to_json_value() { - Ok(event_json) => event_json, - Err(error) => { - record_callback_error(format!( - "nemo_relay: failed to serialize JS event sanitizer context: {error}" - )); - return nemo_relay::api::event::EventSanitizeFields::default(); - } - }; - let sanitized = (|| -> FlowResult<_> { - let value = direct - .call_event_sanitize(event_json, js_event_fields(&fields)) - .map_err(|error| { - FlowError::Internal(format!( - "nemo_relay: JS event sanitizer callback failed: {error}" - )) - })?; - let value = callable::unwrap_middleware_result( - value, - "nemo_relay: JS event sanitizer callback failed", - )?; - let fields = event_sanitize_fields_from_json(value).map_err(|error| { - FlowError::Internal(format!( - "nemo_relay: JS event sanitizer callback failed: invalid JS event sanitizer result: {error}" - )) - })?; - core_event_fields(fields).ok_or_else(|| { - FlowError::Internal( - "nemo_relay: JS event sanitizer callback failed: invalid JS event sanitizer result" - .to_string(), - ) - }) - })(); - match sanitized { - Ok(sanitized) => sanitized, - Err(error) => { - record_callback_error(error.to_string()); - nemo_relay::api::event::EventSanitizeFields::default() - } - } - } else { - background(event, fields) - } - })) + let callback = Arc::new(crate::promise_call::PromiseAwareFn::new(env, func)?); + Ok(callable::wrap_js_event_sanitize_promise_fn(callback)) } type NodeLlmCodec = ( @@ -1857,15 +1747,22 @@ pub fn clear_last_callback_error() { /// Internal test helper: invoke a closed JS tool callback wrapper and return the fallback value. #[napi(js_name = "__testClosedToolCallback")] -pub fn test_closed_tool_callback( +pub async fn test_closed_tool_callback( callback: ThreadsafeFunction<(String, Json), ErrorStrategy::Fatal>, name: String, args: Json, -) -> Json { +) -> Result { clear_recorded_callback_error(); let _ = callback.clone().abort(); let wrapped = callable::wrap_js_tool_fn(callback); - wrapped(&name, args) + let fallback = args.clone(); + match wrapped(name, args).await { + Ok(value) => Ok(value), + Err(error) => { + record_callback_error(error.to_string()); + Ok(fallback) + } + } } /// Internal test helper: model a closed JS LLM request sanitizer. @@ -2767,8 +2664,10 @@ macro_rules! napi_event_guardrail_api { ($register_name:ident, $deregister_name:ident, $core_register:path, $core_deregister:path) => { /// Register an event sanitize guardrail. /// - /// The callback must be synchronous. Callback, serialization, conversion, or - /// invalid-result failures clear the event fields and record the error for + /// The callback may return fields directly or in a Promise. Scope and mark + /// calls queue the event and return synchronously; publication resumes after + /// the Promise settles. Callback, serialization, conversion, or invalid-result + /// failures preserve the original event fields and record the error for /// `getLastCallbackError()`. #[napi] pub fn $register_name( @@ -2776,7 +2675,7 @@ macro_rules! napi_event_guardrail_api { name: String, priority: i32, #[napi( - ts_arg_type = "(event: Json, fields: EventSanitizeFields) => EventSanitizeFields" + ts_arg_type = "(event: Json, fields: EventSanitizeFields) => EventSanitizeFields | Promise" )] guardrail: JsFunction, ) -> Result<()> { @@ -2822,8 +2721,13 @@ macro_rules! napi_guardrail_tool_api { priority: i32, guardrail: JsFunction, ) -> Result<()> { - let callback = middleware_tool_callback_tsfn(&env, &guardrail)?; - $core_register(&name, priority, $wrapper(callback)).map_err(to_napi_err) + let callback = Arc::new(PromiseAwareFn::new(&env, &guardrail)?); + $core_register( + &name, + priority, + callable::wrap_js_tool_sanitize_promise_fn(callback), + ) + .map_err(to_napi_err) } $(#[doc = $dereg_doc])* @@ -2878,13 +2782,20 @@ pub fn register_tool_conditional_execution_guardrail( env: Env, name: String, priority: i32, + #[napi( + ts_arg_type = "(toolName: string, args: Json) => string | null | Promise" + )] guardrail: JsFunction, ) -> Result<()> { - let callback = middleware_tool_callback_tsfn(&env, &guardrail)?; + let callback = std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &guardrail).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + ); core_registry_api::register_tool_conditional_execution_guardrail( &name, priority, - callable::wrap_js_tool_conditional_fn(callback), + callable::wrap_js_tool_conditional_promise_fn(callback), ) .map_err(to_napi_err) } @@ -2914,8 +2825,18 @@ macro_rules! napi_intercept_tool_api { break_chain: bool, callable: JsFunction, ) -> Result<()> { - let callback = middleware_tool_callback_tsfn(&env, &callable)?; - $core_register(&name, priority, break_chain, $wrapper(callback)).map_err(to_napi_err) + let callback = std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &callable).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + ); + $core_register( + &name, + priority, + break_chain, + callable::wrap_js_tool_request_intercept_promise_fn(callback), + ) + .map_err(to_napi_err) } $(#[doc = $dereg_doc])* @@ -3003,15 +2924,16 @@ pub fn register_llm_sanitize_request_guardrail( name: String, priority: i32, #[napi( - ts_arg_type = "(request: Json, context: import('./plugin').LlmSanitizeRequestContext) => Json | null" + ts_arg_type = "(request: Json, context: import('./plugin').LlmSanitizeRequestContext) => Json | null | Promise" )] guardrail: JsFunction, ) -> Result<()> { - let callback = middleware_llm_sanitize_request_callback_tsfn(&env, &guardrail)?; core_registry_api::register_llm_sanitize_request_guardrail( &name, priority, - callable::wrap_js_llm_sanitize_request_fn(callback), + callable::wrap_js_llm_sanitize_request_promise_fn(Arc::new(PromiseAwareFn::new( + &env, &guardrail, + )?)), ) .map_err(to_napi_err) } @@ -3036,15 +2958,16 @@ pub fn register_llm_sanitize_response_guardrail( name: String, priority: i32, #[napi( - ts_arg_type = "(response: Json, context: import('./plugin').LlmSanitizeResponseContext) => Json | null" + ts_arg_type = "(response: Json, context: import('./plugin').LlmSanitizeResponseContext) => Json | null | Promise" )] guardrail: JsFunction, ) -> Result<()> { - let callback = middleware_llm_sanitize_response_callback_tsfn(&env, &guardrail)?; core_registry_api::register_llm_sanitize_response_guardrail( &name, priority, - callable::wrap_js_llm_sanitize_response_fn(callback), + callable::wrap_js_llm_sanitize_response_promise_fn(Arc::new(PromiseAwareFn::new( + &env, &guardrail, + )?)), ) .map_err(to_napi_err) } @@ -3067,13 +2990,18 @@ pub fn register_llm_conditional_execution_guardrail( env: Env, name: String, priority: i32, + #[napi(ts_arg_type = "(request: Json) => string | null | Promise")] guardrail: JsFunction, ) -> Result<()> { - let callback = middleware_json_callback_tsfn(&env, &guardrail)?; + let callback = std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &guardrail).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + ); core_registry_api::register_llm_conditional_execution_guardrail( &name, priority, - callable::wrap_js_llm_conditional_fn(callback), + callable::wrap_js_llm_conditional_promise_fn(callback), ) .map_err(to_napi_err) } @@ -3103,16 +3031,20 @@ pub fn register_llm_request_intercept( priority: i32, break_chain: bool, #[napi( - ts_arg_type = "(args: { name: string; request: Json; annotated: Json | null }) => { request: Json; annotated?: Json | null; pendingMarks?: Array<{ name: string; category?: string | null; categoryProfile?: Json; data?: Json; metadata?: Json }>; optimizationContributions?: Array<{ id?: string; sequence?: number; producer: string; kind: 'input_compression' | 'model_routing' | (string & {}); applied: boolean; model_transition?: { baseline?: { model: string; provider?: string }; effective?: { model: string; provider?: string } }; token_impact?: { baseline?: { prompt_tokens?: number; completion_tokens?: number; cache_read_tokens?: number; cache_write_tokens?: number; total_tokens?: number }; effective?: { prompt_tokens?: number; completion_tokens?: number; cache_read_tokens?: number; cache_write_tokens?: number; total_tokens?: number }; saved?: { prompt_tokens?: number; completion_tokens?: number; cache_read_tokens?: number; cache_write_tokens?: number; total_tokens?: number }; quality?: 'observed' | 'estimated'; estimation_method?: string }; payload_schema?: { name: string; version: string }; payload?: Json; [key: string]: Json | undefined }> }" + ts_arg_type = "(args: { name: string; request: Json; annotated: Json | null }) => import('./plugin').LlmRequestInterceptOutcome | Promise" )] callable: JsFunction, ) -> Result<()> { - let callback = middleware_json_callback_tsfn(&env, &callable)?; + let callback = std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &callable).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + ); core_registry_api::register_llm_request_intercept( &name, priority, break_chain, - callable::wrap_js_llm_request_intercept_fn(callback), + callable::wrap_js_llm_request_intercept_promise_fn(callback), ) .map_err(to_napi_err) } @@ -3245,7 +3177,7 @@ pub fn deregister_subscriber(name: String) -> Result { /// still run. /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this -/// Promise does not block the Node event loop while event sanitizers settle. +/// Promise does not block the Node event loop while Promise-returning event sanitizers settle. /// /// The Promise rejects if the blocking task fails or the core subscriber flush returns an error. /// Callers should handle errors when awaiting it. @@ -3265,8 +3197,10 @@ macro_rules! napi_scope_event_guardrail_api { ($register_name:ident, $deregister_name:ident, $core_register:path, $core_deregister:path) => { /// Register a scope-local event sanitize guardrail. /// - /// The callback must be synchronous. Callback, serialization, conversion, or - /// invalid-result failures clear the event fields and record the error for + /// The callback may return fields directly or in a Promise. Scope and mark + /// calls queue the event and return synchronously; publication resumes after + /// the Promise settles. Callback, serialization, conversion, or invalid-result + /// failures preserve the original event fields and record the error for /// `getLastCallbackError()`. #[napi] pub fn $register_name( @@ -3275,7 +3209,7 @@ macro_rules! napi_scope_event_guardrail_api { name: String, priority: i32, #[napi( - ts_arg_type = "(event: Json, fields: EventSanitizeFields) => EventSanitizeFields" + ts_arg_type = "(event: Json, fields: EventSanitizeFields) => EventSanitizeFields | Promise" )] guardrail: JsFunction, ) -> Result<()> { @@ -3333,8 +3267,14 @@ macro_rules! napi_scope_guardrail_tool_api { ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let callback = middleware_tool_callback_tsfn(&env, &guardrail)?; - $core_register(&uuid, &name, priority, $wrapper(callback)).map_err(to_napi_err) + let callback = Arc::new(PromiseAwareFn::new(&env, &guardrail)?); + $core_register( + &uuid, + &name, + priority, + callable::wrap_js_tool_sanitize_promise_fn(callback), + ) + .map_err(to_napi_err) } $(#[doc = $dereg_doc])* @@ -3402,7 +3342,11 @@ pub fn scope_register_tool_conditional_execution_guardrail( &uuid, &name, priority, - callable::wrap_js_tool_conditional_fn(middleware_tool_callback_tsfn(&env, &guardrail)?), + callable::wrap_js_tool_conditional_promise_fn(std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &guardrail).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + )), ) .map_err(to_napi_err) } @@ -3441,9 +3385,19 @@ macro_rules! napi_scope_intercept_tool_api { ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let callback = middleware_tool_callback_tsfn(&env, &callable)?; - $core_register(&uuid, &name, priority, break_chain, $wrapper(callback)) - .map_err(to_napi_err) + let callback = std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &callable).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + ); + $core_register( + &uuid, + &name, + priority, + break_chain, + callable::wrap_js_tool_request_intercept_promise_fn(callback), + ) + .map_err(to_napi_err) } $(#[doc = $dereg_doc])* @@ -3546,7 +3500,7 @@ pub fn scope_register_llm_sanitize_request_guardrail( name: String, priority: i32, #[napi( - ts_arg_type = "(request: Json, context: import('./plugin').LlmSanitizeRequestContext) => Json | null" + ts_arg_type = "(request: Json, context: import('./plugin').LlmSanitizeRequestContext) => Json | null | Promise" )] guardrail: JsFunction, ) -> Result<()> { @@ -3556,9 +3510,9 @@ pub fn scope_register_llm_sanitize_request_guardrail( &uuid, &name, priority, - callable::wrap_js_llm_sanitize_request_fn(middleware_llm_sanitize_request_callback_tsfn( + callable::wrap_js_llm_sanitize_request_promise_fn(Arc::new(PromiseAwareFn::new( &env, &guardrail, - )?), + )?)), ) .map_err(to_napi_err) } @@ -3590,7 +3544,7 @@ pub fn scope_register_llm_sanitize_response_guardrail( name: String, priority: i32, #[napi( - ts_arg_type = "(response: Json, context: import('./plugin').LlmSanitizeResponseContext) => Json | null" + ts_arg_type = "(response: Json, context: import('./plugin').LlmSanitizeResponseContext) => Json | null | Promise" )] guardrail: JsFunction, ) -> Result<()> { @@ -3600,9 +3554,9 @@ pub fn scope_register_llm_sanitize_response_guardrail( &uuid, &name, priority, - callable::wrap_js_llm_sanitize_response_fn(middleware_llm_sanitize_response_callback_tsfn( + callable::wrap_js_llm_sanitize_response_promise_fn(Arc::new(PromiseAwareFn::new( &env, &guardrail, - )?), + )?)), ) .map_err(to_napi_err) } @@ -3640,7 +3594,11 @@ pub fn scope_register_llm_conditional_execution_guardrail( &uuid, &name, priority, - callable::wrap_js_llm_conditional_fn(middleware_json_callback_tsfn(&env, &guardrail)?), + callable::wrap_js_llm_conditional_promise_fn(std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &guardrail).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + )), ) .map_err(to_napi_err) } @@ -3677,19 +3635,23 @@ pub fn scope_register_llm_request_intercept( priority: i32, break_chain: bool, #[napi( - ts_arg_type = "(args: { name: string; request: Json; annotated: Json | null }) => { request: Json; annotated?: Json | null; pendingMarks?: Array<{ name: string; category?: string | null; categoryProfile?: Json; data?: Json; metadata?: Json }>; optimizationContributions?: Array<{ id?: string; sequence?: number; producer: string; kind: 'input_compression' | 'model_routing' | (string & {}); applied: boolean; model_transition?: { baseline?: { model: string; provider?: string }; effective?: { model: string; provider?: string } }; token_impact?: { baseline?: { prompt_tokens?: number; completion_tokens?: number; cache_read_tokens?: number; cache_write_tokens?: number; total_tokens?: number }; effective?: { prompt_tokens?: number; completion_tokens?: number; cache_read_tokens?: number; cache_write_tokens?: number; total_tokens?: number }; saved?: { prompt_tokens?: number; completion_tokens?: number; cache_read_tokens?: number; cache_write_tokens?: number; total_tokens?: number }; quality?: 'observed' | 'estimated'; estimation_method?: string }; payload_schema?: { name: string; version: string }; payload?: Json; [key: string]: Json | undefined }> }" + ts_arg_type = "(args: { name: string; request: Json; annotated: Json | null }) => import('./plugin').LlmRequestInterceptOutcome | Promise" )] callable: JsFunction, ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let callback = middleware_json_callback_tsfn(&env, &callable)?; + let callback = std::sync::Arc::new( + crate::promise_call::PromiseAwareFn::new(&env, &callable).map_err(|error| { + napi::Error::from_reason(format!("failed to create PromiseAwareFn: {error}")) + })?, + ); core_registry_api::scope_register_llm_request_intercept( &uuid, &name, priority, break_chain, - callable::wrap_js_llm_request_intercept_fn(callback), + callable::wrap_js_llm_request_intercept_promise_fn(callback), ) .map_err(to_napi_err) } @@ -3865,7 +3827,9 @@ pub fn tool_request_intercepts(env: Env, name: String, args: Json) -> Result Result< async move { TASK_SCOPE_STACK .scope(scope_stack, async move { - core_tool_api::tool_conditional_execution(&name, &args).map_err(to_napi_err) + core_tool_api::tool_conditional_execution(&name, &args) + .await + .map_err(to_napi_err) }) .await }, @@ -3905,6 +3871,7 @@ pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result Result { async move { TASK_SCOPE_STACK .scope(scope_stack, async move { - core_llm_api::llm_conditional_execution(&llm_request).map_err(to_napi_err) + core_llm_api::llm_conditional_execution(&llm_request) + .await + .map_err(to_napi_err) }) .await }, diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index ba8abafc2..b40aba073 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -207,6 +207,318 @@ fn recv_middleware_option_string_result( } } +async fn await_middleware_json_result( + rx: tokio::sync::oneshot::Receiver, + error_prefix: &str, +) -> Result { + let value = rx + .await + .map_err(|error| FlowError::Internal(format!("{error_prefix}: {error}")))?; + unwrap_middleware_result(value, error_prefix) +} + +async fn await_middleware_json_or_value( + rx: tokio::sync::oneshot::Receiver, + error_prefix: &str, + fallback: Json, +) -> Json { + match await_middleware_json_result(rx, error_prefix).await { + Ok(value) => value, + Err(error) => { + record_callback_error(error.to_string()); + fallback + } + } +} + +async fn await_middleware_option_string_result( + rx: tokio::sync::oneshot::Receiver, + error_prefix: &str, +) -> Result> { + match await_middleware_json_result(rx, error_prefix).await? { + Json::Null => Ok(None), + Json::String(value) => Ok(Some(value)), + other => Err(FlowError::Internal(format!( + "{error_prefix}: expected string or null, got {other:?}", + ))), + } +} + +/// Wrap a Promise-aware JS `(name, args) => string | null` tool guardrail. +pub fn wrap_js_tool_conditional_promise_fn(func: Arc) -> ToolConditionalFn { + Arc::new(move |name: String, args: Json| { + let func = func.clone(); + Box::pin(async move { + let value = func + .call_spread(vec![Json::String(name), args]) + .await + .inspect_err(|error| record_callback_error(error.to_string()))?; + match value { + Json::Null => Ok(None), + Json::String(reason) => Ok(Some(reason)), + other => { + let error = FlowError::Internal(format!( + "JS tool conditional callback failed: expected string or null, got {other:?}" + )); + record_callback_error(error.to_string()); + Err(error) + } + } + }) + }) +} + +/// Wrap a Promise-aware JS `(name, args) => Json` tool request intercept. +pub fn wrap_js_tool_request_intercept_promise_fn(func: Arc) -> ToolInterceptFn { + Arc::new(move |name: String, args: Json| { + let func = func.clone(); + Box::pin(async move { + func.call_spread(vec![Json::String(name), args]) + .await + .inspect_err(|error| record_callback_error(error.to_string())) + }) + }) +} + +/// Wrap a Promise-aware JS tool sanitizer. +pub fn wrap_js_tool_sanitize_promise_fn(func: Arc) -> ToolSanitizeFn { + Arc::new(move |name: String, value: Json| { + let func = func.clone(); + Box::pin(async move { + func.call_spread(vec![Json::String(name), value]) + .await + .inspect_err(|error| { + record_callback_error(error.to_string()); + }) + }) + }) +} + +/// Wrap a Promise-aware JS LLM request sanitizer. +pub fn wrap_js_llm_sanitize_request_promise_fn(func: Arc) -> LlmSanitizeRequestFn { + Arc::new( + move |request: LlmRequest, context: LlmSanitizeRequestContext| { + let func = func.clone(); + Box::pin(async move { + let request = serde_json::to_value(request).unwrap_or(Json::Null); + let context = js_llm_sanitize_request_context(&context); + let value = func + .call_spread_with_arg0(Box::new(move |env| { + let mut args = env.create_array_with_length(2)?; + let request = unsafe { + JsUnknown::from_raw_unchecked( + env.raw(), + Json::to_napi_value(env.raw(), request)?, + ) + }; + args.set_element(0, request)?; + args.set_element( + 1, + js_llm_sanitize_request_context_to_napi(env, context)?, + )?; + Ok(js_object_to_unknown(env, args)) + })) + .await + .inspect_err(|error| { + record_callback_error(error.to_string()); + })?; + if value.is_null() { + Ok(None) + } else { + serde_json::from_value(value) + .map(Some) + .map_err(|error| { + let error = FlowError::Internal(format!( + "JS LLM sanitize request callback failed: failed to deserialize LlmRequest: {error}" + )); + record_callback_error(error.to_string()); + error + }) + } + }) + }, + ) +} + +/// Wrap a Promise-aware JS LLM response sanitizer. +pub fn wrap_js_llm_sanitize_response_promise_fn( + func: Arc, +) -> LlmSanitizeResponseFn { + Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { + let func = func.clone(); + Box::pin(async move { + let context = js_llm_sanitize_response_context(&context); + let value = func + .call_spread_with_arg0(Box::new(move |env| { + let mut args = env.create_array_with_length(2)?; + let response = unsafe { + JsUnknown::from_raw_unchecked( + env.raw(), + Json::to_napi_value(env.raw(), response)?, + ) + }; + args.set_element(0, response)?; + args.set_element(1, js_llm_sanitize_response_context_to_napi(env, context)?)?; + Ok(js_object_to_unknown(env, args)) + })) + .await + .inspect_err(|error| { + record_callback_error(error.to_string()); + })?; + Ok((!value.is_null()).then_some(value)) + }) + }) +} + +/// Wrap a Promise-aware JS `(request) => string | null` LLM guardrail. +pub fn wrap_js_llm_conditional_promise_fn(func: Arc) -> LlmConditionalFn { + Arc::new(move |request: LlmRequest| { + let func = func.clone(); + Box::pin(async move { + let value = func + .call(serde_json::to_value(request).unwrap_or(Json::Null)) + .await + .inspect_err(|error| record_callback_error(error.to_string()))?; + match value { + Json::Null => Ok(None), + Json::String(reason) => Ok(Some(reason)), + other => { + let error = FlowError::Internal(format!( + "JS LLM conditional callback failed: expected string or null, got {other:?}" + )); + record_callback_error(error.to_string()); + Err(error) + } + } + }) + }) +} + +/// Wrap a Promise-aware JS LLM request intercept. +pub fn wrap_js_llm_request_intercept_promise_fn( + func: Arc, +) -> LlmRequestInterceptFn { + Arc::new( + move |name: String, request: LlmRequest, annotated: Option| { + let func = func.clone(); + Box::pin(async move { + let value = func + .call(serde_json::json!({ + "name": name, + "request": request, + "annotated": annotated, + })) + .await + .inspect_err(|error| { + record_callback_error(error.to_string()); + })?; + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct JsOutcome { + request: LlmRequest, + #[serde(default)] + annotated: Option, + #[serde(default)] + pending_marks: Vec, + #[serde(default)] + optimization_contributions: Vec, + } + let outcome: JsOutcome = serde_json::from_value(value).map_err(|error| { + let error = FlowError::Internal(format!( + "invalid JS LLM request intercept outcome: {error}" + )); + record_callback_error(error.to_string()); + error + })?; + Ok(LlmRequestInterceptOutcome { + request: outcome.request, + annotated_request: outcome.annotated, + pending_marks: outcome.pending_marks.into_iter().map(Into::into).collect(), + optimization_contributions: outcome.optimization_contributions, + }) + }) + }, + ) +} + +/// Wrap a Promise-aware JS event sanitizer. +/// +/// Event sanitizers run on Relay's serial publication dispatcher, not on the +/// JavaScript registration thread. Waiting here therefore preserves synchronous +/// scope/mark APIs while allowing the JavaScript callback to settle a Promise +/// on the Node event loop. +pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSanitizeFn { + Arc::new(move |event: Event, fields: CoreEventSanitizeFields| { + let func = func.clone(); + Box::pin(async move { + let event_json = JsEvent::try_from_event(&event) + .map(JsEvent::into_json) + .map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS event sanitizer context: {error}" + )); + record_callback_error(error.to_string()); + error + })?; + let js_fields = EventSanitizeFields { + data: fields.data, + category_profile: fields + .category_profile + .as_ref() + .map(serde_json::to_value) + .transpose() + .map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS event sanitizer category profile: {error}" + )); + record_callback_error(error.to_string()); + error + })?, + metadata: fields.metadata, + }; + let value = func + .call_spread(vec![ + event_json, + serde_json::to_value(js_fields).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS event sanitizer fields: {error}" + )); + record_callback_error(error.to_string()); + error + })?, + ]) + .await + .inspect_err(|error| { + // Scope and mark publication happens on the dispatcher + // thread. Preserve the event (the core fails open) while + // making the binding-visible failure available to Node. + record_callback_error(error.to_string()); + })?; + let fields = event_sanitize_fields_from_json(value).map_err(|error| { + let error = + FlowError::Internal(format!("invalid JS event sanitizer result: {error}")); + record_callback_error(error.to_string()); + error + })?; + let category_profile = fields + .category_profile + .map(serde_json::from_value) + .transpose() + .map_err(|error| { + let error = + FlowError::Internal(format!("invalid JS event sanitizer result: {error}")); + record_callback_error(error.to_string()); + error + })?; + Ok(CoreEventSanitizeFields { + data: fields.data, + category_profile, + metadata: fields.metadata, + }) + }) + }) +} + fn recv_json_or_null(rx: std::sync::mpsc::Receiver, error_prefix: &str) -> Json { rx.recv().unwrap_or_else(|e| { record_callback_error(format!("{error_prefix}: {e}")); @@ -249,28 +561,25 @@ pub fn wrap_js_tool_fn( func: ThreadsafeFunction<(String, Json), ErrorStrategy::Fatal>, ) -> ToolSanitizeFn { let func = Arc::new(func); - Arc::new(move |name: &str, args: Json| { + Arc::new(move |name: String, args: Json| { let func = func.clone(); - let name = name.to_string(); - let fallback = args.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - let status = func.call_with_return_value( - (name, args), - ThreadsafeFunctionCallMode::Blocking, - move |val: Option| { - let _ = tx.send(callback_json(val)); - Ok(()) - }, - ); - if status != napi::Status::Ok { - record_callback_error(format!( - "nemo_relay: failed to queue JS tool callback: {status:?}" - )); - return fallback; - } - // TODO: This closure returns Json (not Result), so we cannot propagate - // errors through the type system. Log the error so failures are not silent. - recv_middleware_json_or_value(rx, "nemo_relay: JS tool callback failed", fallback) + Box::pin(async move { + let (tx, rx) = tokio::sync::oneshot::channel(); + let status = func.call_with_return_value( + (name, args), + ThreadsafeFunctionCallMode::Blocking, + move |val: Option| { + let _ = tx.send(callback_json(val)); + Ok(()) + }, + ); + if status != napi::Status::Ok { + return Err(FlowError::Internal(format!( + "failed to queue JS tool callback: {status:?}" + ))); + } + await_middleware_json_result(rx, "nemo_relay: JS tool callback failed").await + }) }) } @@ -279,25 +588,25 @@ pub fn wrap_js_tool_conditional_fn( func: ThreadsafeFunction<(String, Json), ErrorStrategy::Fatal>, ) -> ToolConditionalFn { let func = Arc::new(func); - Arc::new(move |name: &str, args: &Json| { + Arc::new(move |name: String, args: Json| { let func = func.clone(); - let name = name.to_string(); - let args = args.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - let status = func.call_with_return_value( - (name, args), - ThreadsafeFunctionCallMode::Blocking, - move |val: Option| { - let _ = tx.send(callback_json(val)); - Ok(()) - }, - ); - if status != napi::Status::Ok { - return Err(FlowError::Internal(format!( - "failed to queue JS tool conditional callback: {status:?}", - ))); - } - recv_middleware_option_string_result(rx, "JS tool conditional callback failed") + Box::pin(async move { + let (tx, rx) = tokio::sync::oneshot::channel(); + let status = func.call_with_return_value( + (name, args), + ThreadsafeFunctionCallMode::Blocking, + move |val: Option| { + let _ = tx.send(callback_json(val)); + Ok(()) + }, + ); + if status != napi::Status::Ok { + return Err(FlowError::Internal(format!( + "failed to queue JS tool conditional callback: {status:?}", + ))); + } + await_middleware_option_string_result(rx, "JS tool conditional callback failed").await + }) }) } @@ -306,24 +615,25 @@ pub fn wrap_js_tool_request_intercept_fn( func: ThreadsafeFunction<(String, Json), ErrorStrategy::Fatal>, ) -> ToolInterceptFn { let func = Arc::new(func); - Arc::new(move |name: &str, args: Json| { + Arc::new(move |name: String, args: Json| { let func = func.clone(); - let name = name.to_string(); - let (tx, rx) = std::sync::mpsc::channel(); - let status = func.call_with_return_value( - (name, args), - ThreadsafeFunctionCallMode::Blocking, - move |val: Option| { - let _ = tx.send(callback_json(val)); - Ok(()) - }, - ); - if status != napi::Status::Ok { - return Err(FlowError::Internal(format!( - "failed to queue JS tool callback: {status:?}", - ))); - } - recv_middleware_json_result(rx, "JS tool callback failed") + Box::pin(async move { + let (tx, rx) = tokio::sync::oneshot::channel(); + let status = func.call_with_return_value( + (name, args), + ThreadsafeFunctionCallMode::Blocking, + move |val: Option| { + let _ = tx.send(callback_json(val)); + Ok(()) + }, + ); + if status != napi::Status::Ok { + return Err(FlowError::Internal(format!( + "failed to queue JS tool callback: {status:?}", + ))); + } + await_middleware_json_result(rx, "JS tool callback failed").await + }) }) } @@ -367,57 +677,57 @@ pub fn wrap_js_llm_request_intercept_fn( ) -> LlmRequestInterceptFn { let func = Arc::new(func); Arc::new( - move |name: &str, - request: LlmRequest, - annotated: Option| - -> Result { + move |name: String, request: LlmRequest, annotated: Option| { let func = func.clone(); - let req_json = serde_json::to_value(&request).unwrap_or(Json::Null); - let annotated_json = annotated - .as_ref() - .map(|a| serde_json::to_value(a).unwrap_or(Json::Null)) - .unwrap_or(Json::Null); - let arg = serde_json::json!({ - "name": name, - "request": req_json, - "annotated": annotated_json, - }); - let (tx, rx) = std::sync::mpsc::channel(); - let status = func.call_with_return_value( - arg, - ThreadsafeFunctionCallMode::Blocking, - move |val: Option| { - let _ = tx.send(callback_json(val)); - Ok(()) - }, - ); - if status != napi::Status::Ok { - return Err(FlowError::Internal(format!( - "failed to queue JS LLM request intercept callback: {status:?}", - ))); - } - let result = - recv_middleware_json_result(rx, "JS LLM request intercept callback failed")?; + Box::pin(async move { + let req_json = serde_json::to_value(&request).unwrap_or(Json::Null); + let annotated_json = annotated + .as_ref() + .map(|a| serde_json::to_value(a).unwrap_or(Json::Null)) + .unwrap_or(Json::Null); + let arg = serde_json::json!({ + "name": name, + "request": req_json, + "annotated": annotated_json, + }); + let (tx, rx) = tokio::sync::oneshot::channel(); + let status = func.call_with_return_value( + arg, + ThreadsafeFunctionCallMode::Blocking, + move |val: Option| { + let _ = tx.send(callback_json(val)); + Ok(()) + }, + ); + if status != napi::Status::Ok { + return Err(FlowError::Internal(format!( + "failed to queue JS LLM request intercept callback: {status:?}", + ))); + } + let result = + await_middleware_json_result(rx, "JS LLM request intercept callback failed") + .await?; - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct JsOutcome { - request: LlmRequest, - #[serde(default)] - annotated: Option, - #[serde(default)] - pending_marks: Vec, - #[serde(default)] - optimization_contributions: Vec, - } - let outcome: JsOutcome = serde_json::from_value(result).map_err(|e| { - FlowError::Internal(format!("invalid JS LLM request intercept outcome: {e}")) - })?; - Ok(LlmRequestInterceptOutcome { - request: outcome.request, - annotated_request: outcome.annotated, - pending_marks: outcome.pending_marks.into_iter().map(Into::into).collect(), - optimization_contributions: outcome.optimization_contributions, + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct JsOutcome { + request: LlmRequest, + #[serde(default)] + annotated: Option, + #[serde(default)] + pending_marks: Vec, + #[serde(default)] + optimization_contributions: Vec, + } + let outcome: JsOutcome = serde_json::from_value(result).map_err(|e| { + FlowError::Internal(format!("invalid JS LLM request intercept outcome: {e}")) + })?; + Ok(LlmRequestInterceptOutcome { + request: outcome.request, + annotated_request: outcome.annotated, + pending_marks: outcome.pending_marks.into_iter().map(Into::into).collect(), + optimization_contributions: outcome.optimization_contributions, + }) }) }, ) @@ -431,11 +741,66 @@ pub fn wrap_js_llm_sanitize_request_fn( let func = Arc::new(func); Arc::new( move |request: LlmRequest, context: LlmSanitizeRequestContext| { - let context = js_llm_sanitize_request_context(&context); - let request = serde_json::to_value(request).unwrap_or(Json::Null); - let (tx, rx) = std::sync::mpsc::channel(); + let func = func.clone(); + Box::pin(async move { + let context = js_llm_sanitize_request_context(&context); + let request = serde_json::to_value(request).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM sanitize request: {error}" + )); + record_callback_error(error.to_string()); + error + })?; + let (tx, rx) = tokio::sync::oneshot::channel(); + if func.call_with_return_value( + (request.clone(), context), + ThreadsafeFunctionCallMode::Blocking, + move |value: Option| { + let _ = tx.send(callback_json(value)); + Ok(()) + }, + ) != napi::Status::Ok + { + record_callback_error( + "nemo_relay: failed to queue JS LLM sanitize request callback", + ); + return Err(FlowError::Internal( + "failed to queue JS LLM sanitize request callback".into(), + )); + } + let value = await_middleware_json_result( + rx, + "nemo_relay: JS LLM request sanitizer callback failed", + ) + .await + .inspect_err(|error| record_callback_error(error.to_string()))?; + if value.is_null() { + return Ok(None); + } + serde_json::from_value(value) + .map(Some) + .map_err(|error| FlowError::Internal(format!( + "JS LLM sanitize request callback failed: failed to deserialize LlmRequest: {error}" + ))) + .inspect_err(|error| record_callback_error(error.to_string())) + }) + }, + ) +} + +/// Wrap a JS function for LLM response sanitization. The callback receives +/// `(response, context)`; returning `null` omits the event payload. +pub fn wrap_js_llm_sanitize_response_fn( + func: ThreadsafeFunction<(Json, JsLlmSanitizeResponseContext), ErrorStrategy::Fatal>, +) -> LlmSanitizeResponseFn { + let func = Arc::new(func); + Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { + let func = func.clone(); + Box::pin(async move { + let context = js_llm_sanitize_response_context(&context); + let (tx, rx) = tokio::sync::oneshot::channel(); if func.call_with_return_value( - (request.clone(), context), + (response, context), ThreadsafeFunctionCallMode::Blocking, move |value: Option| { let _ = tx.send(callback_json(value)); @@ -444,58 +809,20 @@ pub fn wrap_js_llm_sanitize_request_fn( ) != napi::Status::Ok { record_callback_error( - "nemo_relay: failed to queue JS LLM sanitize request callback", + "nemo_relay: failed to queue JS LLM sanitize response callback", ); - return None; + return Err(FlowError::Internal( + "failed to queue JS LLM sanitize response callback".into(), + )); } - let value = recv_middleware_json_or_value( + let value = await_middleware_json_result( rx, - "nemo_relay: JS LLM request sanitizer callback failed", - Json::Null, - ); - if value.is_null() { - return None; - } - serde_json::from_value(value).map_or_else( - |error| { - record_callback_error(format!( - "nemo_relay: JS LLM sanitize request callback failed: failed to deserialize LlmRequest: {error}" - )); - None - }, - Some, - ) - }, - ) -} - -/// Wrap a JS function for LLM response sanitization. The callback receives -/// `(response, context)`; returning `null` omits the event payload. -pub fn wrap_js_llm_sanitize_response_fn( - func: ThreadsafeFunction<(Json, JsLlmSanitizeResponseContext), ErrorStrategy::Fatal>, -) -> LlmSanitizeResponseFn { - let func = Arc::new(func); - Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { - let context = js_llm_sanitize_response_context(&context); - let (tx, rx) = std::sync::mpsc::channel(); - if func.call_with_return_value( - (response, context), - ThreadsafeFunctionCallMode::Blocking, - move |value: Option| { - let _ = tx.send(callback_json(value)); - Ok(()) - }, - ) != napi::Status::Ok - { - record_callback_error("nemo_relay: failed to queue JS LLM sanitize response callback"); - return None; - } - let value = recv_middleware_json_or_value( - rx, - "nemo_relay: JS LLM response sanitizer callback failed", - Json::Null, - ); - Some(value).and_then(|value| (!value.is_null()).then_some(value)) + "nemo_relay: JS LLM response sanitizer callback failed", + ) + .await + .inspect_err(|error| record_callback_error(error.to_string()))?; + Ok((!value.is_null()).then_some(value)) + }) }) } @@ -649,24 +976,26 @@ pub fn wrap_js_llm_conditional_fn( func: ThreadsafeFunction, ) -> LlmConditionalFn { let func = Arc::new(func); - Arc::new(move |request: &LlmRequest| { + Arc::new(move |request: LlmRequest| { let func = func.clone(); - let req_json = serde_json::to_value(request).unwrap_or(Json::Null); - let (tx, rx) = std::sync::mpsc::channel(); - let status = func.call_with_return_value( - req_json, - ThreadsafeFunctionCallMode::Blocking, - move |val: Option| { - let _ = tx.send(callback_json(val)); - Ok(()) - }, - ); - if status != napi::Status::Ok { - return Err(FlowError::Internal(format!( - "failed to queue JS LLM conditional callback: {status:?}", - ))); - } - recv_middleware_option_string_result(rx, "JS LLM conditional callback failed") + Box::pin(async move { + let req_json = serde_json::to_value(request).unwrap_or(Json::Null); + let (tx, rx) = tokio::sync::oneshot::channel(); + let status = func.call_with_return_value( + req_json, + ThreadsafeFunctionCallMode::Blocking, + move |val: Option| { + let _ = tx.send(callback_json(val)); + Ok(()) + }, + ); + if status != napi::Status::Ok { + return Err(FlowError::Internal(format!( + "failed to queue JS LLM conditional callback: {status:?}", + ))); + } + await_middleware_option_string_result(rx, "JS LLM conditional callback failed").await + }) }) } @@ -783,72 +1112,93 @@ pub fn wrap_js_event_sanitize_fn( func: ThreadsafeFunction<(Json, Json), ErrorStrategy::Fatal>, ) -> EventSanitizeFn { let func = Arc::new(func); - Arc::new(move |event: &Event, fields: CoreEventSanitizeFields| { - let event_json = match JsEvent::try_from_event(event) { - Ok(event) => event.into_json(), - Err(error) => { + Arc::new(move |event: Event, fields: CoreEventSanitizeFields| { + let func = func.clone(); + Box::pin(async move { + let event_json = match JsEvent::try_from_event(&event) { + Ok(event) => event.into_json(), + Err(error) => { + record_callback_error(format!( + "nemo_relay: failed to serialize JS event sanitizer context: {error}" + )); + return Err(FlowError::Internal(error.to_string())); + } + }; + let js_fields = EventSanitizeFields { + data: fields.data.clone(), + category_profile: fields + .category_profile + .as_ref() + .map(serde_json::to_value) + .transpose() + .map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS event sanitizer category profile: {error}" + )); + record_callback_error(error.to_string()); + error + })?, + metadata: fields.metadata.clone(), + }; + let js_fields = serde_json::to_value(js_fields).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS event sanitizer fields: {error}" + )); + record_callback_error(error.to_string()); + error + })?; + let (tx, rx) = tokio::sync::oneshot::channel(); + let status = func.call_with_return_value( + (event_json, js_fields), + ThreadsafeFunctionCallMode::Blocking, + move |value: Option| { + let _ = tx.send(callback_json(value)); + Ok(()) + }, + ); + if status != napi::Status::Ok { record_callback_error(format!( - "nemo_relay: failed to serialize JS event sanitizer context: {error}" + "nemo_relay: failed to queue JS event sanitizer callback: {status:?}" )); - return CoreEventSanitizeFields::default(); + return Err(FlowError::Internal(format!( + "failed to queue JS event sanitizer callback: {status:?}" + ))); } - }; - let js_fields = EventSanitizeFields { - data: fields.data.clone(), - category_profile: fields - .category_profile - .as_ref() - .and_then(|value| serde_json::to_value(value).ok()), - metadata: fields.metadata.clone(), - }; - let (tx, rx) = std::sync::mpsc::channel(); - let status = func.call_with_return_value( - ( - event_json, - serde_json::to_value(js_fields).unwrap_or(Json::Null), - ), - ThreadsafeFunctionCallMode::Blocking, - move |value: Option| { - let _ = tx.send(callback_json(value)); - Ok(()) - }, - ); - if status != napi::Status::Ok { - record_callback_error(format!( - "nemo_relay: failed to queue JS event sanitizer callback: {status:?}" - )); - return CoreEventSanitizeFields::default(); - } - let sanitized = (|| -> Result<_> { - let result = - recv_middleware_json_result(rx, "nemo_relay: JS event sanitizer callback failed")?; - let result = event_sanitize_fields_from_json(result).map_err(|error| { - FlowError::Internal(format!( - "nemo_relay: invalid JS event sanitizer result: {error}" - )) - })?; - let category_profile = result - .category_profile - .map(serde_json::from_value) - .transpose() - .map_err(|error| { + let sanitized: Result = async { + let result = await_middleware_json_result( + rx, + "nemo_relay: JS event sanitizer callback failed", + ) + .await?; + let result = event_sanitize_fields_from_json(result).map_err(|error| { FlowError::Internal(format!( "nemo_relay: invalid JS event sanitizer result: {error}" )) })?; - Ok(CoreEventSanitizeFields { - data: result.data, - category_profile, - metadata: result.metadata, - }) - })(); - match sanitized { - Ok(sanitized) => sanitized, - Err(error) => { - record_callback_error(error.to_string()); - CoreEventSanitizeFields::default() + let category_profile = result + .category_profile + .map(serde_json::from_value) + .transpose() + .map_err(|error| { + FlowError::Internal(format!( + "nemo_relay: invalid JS event sanitizer result: {error}" + )) + })?; + Ok(CoreEventSanitizeFields { + data: result.data, + category_profile, + metadata: result.metadata, + }) } - } + .await; + match sanitized { + Ok(sanitized) => Ok(sanitized), + Err(error) => { + record_callback_error(error.to_string()); + Err(error) + } + } + }) }) } diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index 891373dbf..48b301883 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -66,14 +66,26 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { }, promise(fn) { - return function __nemo_relay_promise_wrapper(error, arg0, next, resolve, reject) { + return function __nemo_relay_promise_wrapper(error, arg0, spread, next, resolve, reject) { if (error != null) { reject(error); return; } Promise.resolve().then(() => ( - next === undefined ? fn(arg0) : fn(arg0, next) - )).then((value) => jsonValue(value === undefined ? null : value)).then(resolve, reject); + next === undefined + ? (spread ? fn(...arg0) : fn(arg0)) + : (spread ? fn(...arg0, next) : fn(arg0, next)) + )).then((value) => jsonValue(value === undefined ? null : value)).then(resolve, (error) => { + let message = 'unknown error'; + try { + if (typeof error === 'string') { + message = error; + } else if (error != null && typeof error.message === 'string') { + message = error.message; + } + } catch {} + reject(message); + }); }; }, }; diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index bb5435207..cdc15bada 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -53,6 +53,7 @@ enum PrimaryArg { struct CallArgs { arg0: PrimaryArg, + spread: bool, next: Option, completion: CallCompletion, } @@ -76,19 +77,6 @@ impl CallCompletion { } } -fn rejection_message( - string_result: napi::Result, - object_message_result: Option>, -) -> String { - if let Ok(value) = string_result { - value - } else if let Some(message_result) = object_message_result { - message_result.unwrap_or_else(|_| "unknown error".to_string()) - } else { - "unknown error".to_string() - } -} - fn closed_tsfn_error() -> FlowError { FlowError::Internal("PromiseAwareFn threadsafe function closed".into()) } @@ -168,12 +156,12 @@ fn build_completion_unknowns( })?; let reject = env.create_function_from_closure("__nemo_relay_reject", move |ctx| { - let message = rejection_message( - ctx.get::(0), - ctx.get::(0) - .ok() - .map(|value| value.get_named_property::("message")), - ); + // Do not invoke arbitrary `error.message` getters here. A throwing + // getter used to escape this callback and abort the N-API call rather + // than settling the middleware future as a rejection. + let message = ctx + .get::(0) + .unwrap_or_else(|_| "unknown error".to_string()); completion.send(Err(FlowError::Internal(message))); ctx.env.get_undefined() })?; @@ -208,7 +196,13 @@ impl PromiseAwareFn { PrimaryArg::Build(build) => build(&ctx.env)?, }; - let args = vec![arg0, next, resolve, reject]; + let spread = unsafe { + JsUnknown::from_raw_unchecked( + ctx.env.raw(), + ctx.env.get_boolean(ctx.value.spread)?.raw(), + ) + }; + let args = vec![arg0, spread, next, resolve, reject]; Ok(args) })?; @@ -222,7 +216,17 @@ impl PromiseAwareFn { /// Call the JS function with the given args and await the result. pub async fn call(&self, args: Json) -> FlowResult { - self.call_inner(PrimaryArg::Json(args), None).await + self.call_inner(PrimaryArg::Json(args), false, None).await + } + + /// Call a JavaScript callback with several JSON arguments. + /// + /// This retains the normal callback shape for middleware such as tool + /// guardrails, whose public contract is `(name, payload)` rather than a + /// single envelope object. + pub async fn call_spread(&self, args: Vec) -> FlowResult { + self.call_inner(PrimaryArg::Json(Json::Array(args)), true, None) + .await } /// Call the JS function with a builder-constructed first argument and await @@ -232,13 +236,20 @@ impl PromiseAwareFn { /// cannot cross the threadsafe-function boundary as plain JSON, such as a /// `#[napi]` class instance. pub async fn call_with_arg0(&self, build_arg0: Arg0Builder) -> FlowResult { - self.call_inner(PrimaryArg::Build(build_arg0), None).await + self.call_inner(PrimaryArg::Build(build_arg0), false, None) + .await + } + + /// Call a JavaScript callback with builder-constructed spread arguments. + pub async fn call_spread_with_arg0(&self, build_arg0: Arg0Builder) -> FlowResult { + self.call_inner(PrimaryArg::Build(build_arg0), true, None) + .await } /// Call the JS function with a middleware-style `next(arg)` callback that /// resolves to a JSON result. pub async fn call_with_json_next(&self, args: Json, next: JsonNextFn) -> FlowResult { - self.call_inner(PrimaryArg::Json(args), Some(NextFn::Json(next))) + self.call_inner(PrimaryArg::Json(args), false, Some(NextFn::Json(next))) .await } @@ -249,7 +260,7 @@ impl PromiseAwareFn { args: Json, next: JsonStreamNextFn, ) -> FlowResult { - self.call_inner(PrimaryArg::Json(args), Some(NextFn::Stream(next))) + self.call_inner(PrimaryArg::Json(args), false, Some(NextFn::Stream(next))) .await } @@ -260,7 +271,12 @@ impl PromiseAwareFn { } } - async fn call_inner(&self, arg0: PrimaryArg, next: Option) -> FlowResult { + async fn call_inner( + &self, + arg0: PrimaryArg, + spread: bool, + next: Option, + ) -> FlowResult { let (sender, receiver) = tokio::sync::oneshot::channel(); let tsfn = self .tsfn @@ -272,6 +288,7 @@ impl PromiseAwareFn { let status = tsfn.call( Ok(CallArgs { arg0, + spread, next, completion: CallCompletion::new(sender), }), diff --git a/crates/node/tests/callback_error_tests.mjs b/crates/node/tests/callback_error_tests.mjs index 83e684a5f..c05eccef9 100644 --- a/crates/node/tests/callback_error_tests.mjs +++ b/crates/node/tests/callback_error_tests.mjs @@ -67,11 +67,11 @@ describe('callback error helpers', () => { } }); - it('closed tool sanitize callbacks preserve the original payload and record the queue failure', () => { + it('closed tool sanitize callbacks preserve the original payload and record the queue failure', async () => { const args = { value: 1, }; - const result = __testClosedToolCallback( + const result = await __testClosedToolCallback( () => ({ ok: true, }), diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 3679b52cf..c5d6d75ea 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -25,10 +25,10 @@ async function waitFor(events, count) { assert.ok(events.length >= count, `expected ${count} events, received ${events.length}`); } -function assertSanitizerFieldsCleared(event) { - assert.equal(event.data, null); - assert.equal(event.category_profile, null); - assert.equal(event.metadata, null); +function assertSanitizerFieldsPreserved(event, expectedData, expectedMetadata = expectedData) { + assert.deepEqual(event.data, expectedData); + assert.equal(event.category_profile?.subtype, 'seeded'); + assert.deepEqual(event.metadata, expectedMetadata); } async function initializeWithoutDiscoveredPluginConfig(config) { @@ -107,13 +107,35 @@ describe('event sanitizer registries', () => { assert.ok(lifecycle.every((event) => event.category_profile.subtype === 'sanitized')); }); - it('fails closed and records invalid direct sanitizer results', async () => { + it('awaits Promise-returning mark sanitizers without making event() asynchronous', async () => { + const events = capture('node-event-sanitize-promise-sub'); + let settled = false; + lib.registerMarkSanitizeGuardrail('node-event-promise', 0, async (_event, fields) => { + await new Promise((resolve) => setImmediate(resolve)); + settled = true; + return { ...fields, data: { sanitized: true } }; + }); + try { + const result = lib.event('promise-checkpoint', null, { raw: true }); + assert.equal(result, undefined); + assert.equal(settled, false); + await lib.flushSubscribers(); + await waitFor(events, 1); + } finally { + lib.deregisterMarkSanitizeGuardrail('node-event-promise'); + lib.deregisterSubscriber('node-event-sanitize-promise-sub'); + } + assert.equal(settled, true); + assert.deepEqual(events.at(-1).data, { sanitized: true }); + }); + + it('fails open and records invalid sanitizer results', async () => { const events = capture('node-event-sanitize-invalid-sub'); const invalidResults = { scalar: () => 'invalid', emptyObject: () => ({}), array: () => [], - promise: () => Promise.resolve({ data: { changed: true } }), + promise: () => Promise.resolve([]), }; try { for (const [kind, sanitizer] of Object.entries(invalidResults)) { @@ -135,7 +157,7 @@ describe('event sanitizer registries', () => { lib.deregisterMarkSanitizeGuardrail(seedName); lib.deregisterMarkSanitizeGuardrail(name); } - assertSanitizerFieldsCleared(events.at(-1)); + assertSanitizerFieldsPreserved(events.at(-1), { kept: kind }); assert.match(lib.getLastCallbackError(), /invalid JS event sanitizer result/); } } finally { @@ -163,12 +185,12 @@ describe('event sanitizer registries', () => { assert.equal(start.metadata.background, true); }); - it('fails closed and records invalid thread-safe sanitizer results', async () => { + it('fails open and records invalid queued sanitizer results', async () => { const events = capture('node-event-sanitize-background-invalid-sub'); const invalidResults = { emptyObject: () => ({}), array: () => [], - promise: () => Promise.resolve({ data: { changed: true } }), + promise: () => Promise.resolve([]), }; try { for (const [kind, sanitizer] of Object.entries(invalidResults)) { @@ -193,7 +215,7 @@ describe('event sanitizer registries', () => { const start = events.find( (event) => event.kind === 'scope' && event.name === name && event.scope_category === 'start', ); - assertSanitizerFieldsCleared(start); + assertSanitizerFieldsPreserved(start, { kept: kind }); assert.match(lib.getLastCallbackError(), /invalid JS event sanitizer result/); } } finally { @@ -201,7 +223,7 @@ describe('event sanitizer registries', () => { } }); - it('fails closed when a thread-safe sanitizer throws', async () => { + it('fails open when a queued sanitizer throws', async () => { const events = capture('node-event-sanitize-background-throw-sub'); lib.clearLastCallbackError(); lib.registerScopeSanitizeStartGuardrail('node-background-throw-seed', -1, (_event, fields) => ({ @@ -220,7 +242,7 @@ describe('event sanitizer registries', () => { const start = events.find( (event) => event.kind === 'scope' && event.name === 'background-throw-tool' && event.scope_category === 'start', ); - assertSanitizerFieldsCleared(start); + assertSanitizerFieldsPreserved(start, { kept: true }); assert.match(lib.getLastCallbackError() ?? '', /background sanitizer boom/i); } finally { lib.deregisterScopeSanitizeStartGuardrail('node-background-throw-seed'); @@ -289,7 +311,7 @@ describe('event sanitizer registries', () => { assert.deepEqual(marks.cleared.data, { raw: true }); }); - it('fails closed when a plugin-owned sanitizer throws', async () => { + it('fails open when a plugin-owned sanitizer throws', async () => { const kind = `node.test.event-sanitize-throw.${Date.now()}`; const events = capture('node-event-sanitize-plugin-throw-sub'); plugin.register(kind, { @@ -314,7 +336,7 @@ describe('event sanitizer registries', () => { lib.event('plugin-throw', null, { raw: true }, { raw: true }); await lib.flushSubscribers(); await waitFor(events, 1); - assertSanitizerFieldsCleared(events.at(-1)); + assertSanitizerFieldsPreserved(events.at(-1), { raw: true }); assert.match(lib.getLastCallbackError() ?? '', /plugin sanitizer boom/i); } finally { plugin.clear(); diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index 61eabb89b..c83c78cb9 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -57,6 +57,17 @@ async function flushSubscriberCallbacks() { } } +async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { + flushSubscribers(); + const deadline = Date.now() + timeoutMs; + while (!predicate()) { + if (Date.now() >= deadline) { + throw new Error('timed out waiting for subscriber callbacks'); + } + await new Promise((resolve) => setImmediate(resolve)); + } +} + function makeNative() { return { headers: {}, @@ -291,7 +302,10 @@ describe('LLM execute', () => { /llm status failure/, ); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks( + () => events.some((e) => e.name === 'exec_status_ok_llm' && e.scope_category === 'end') + && events.some((e) => e.name === 'exec_status_error_llm' && e.scope_category === 'end'), + ); const okEnd = events.find( (e) => e.name === 'exec_status_ok_llm' && e.kind === 'scope' && e.category === 'llm' && e.scope_category === 'end', @@ -386,7 +400,11 @@ describe('LLM guardrails', () => { assert.deepEqual(result, { ok: true }); assert.equal(requestContextChecked, true); assert.equal(responseContextChecked, true); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks( + () => + events.some((event) => event.name === 'contextual_sanitize_llm' && event.scope_category === 'start') && + events.some((event) => event.name === 'contextual_sanitize_llm' && event.scope_category === 'end'), + ); const start = events.find( (event) => event.name === 'contextual_sanitize_llm' && event.scope_category === 'start', ); @@ -718,7 +736,7 @@ describe('LLM guardrails', () => { } }); - it('sanitize request guardrail failures omit the payload and remain usable', async () => { + it('sanitize request guardrail failures preserve the payload and remain usable', async () => { const events = []; clearLastCallbackError(); registerSubscriber('node_llm_san_req_throw_sub', (event) => events.push(event)); @@ -728,7 +746,16 @@ describe('LLM guardrails', () => { try { const request = makeNative(); await llmCallExecute('llm_san_req_throw', request, () => ({ ok: true }), null, null, null, null, null); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks( + () => + events.some( + (event) => + event.name === 'llm_san_req_throw' && + event.kind === 'scope' && + event.category === 'llm' && + event.scope_category === 'start', + ), + ); const start = events.find( (event) => event.name === 'llm_san_req_throw' && @@ -736,8 +763,8 @@ describe('LLM guardrails', () => { event.category === 'llm' && event.scope_category === 'start', ); - assert.equal(start.data, null); - assert.match(getLastCallbackError() ?? '', /JavaScript callback threw/i); + assert.deepEqual(start.data, { headers: request.headers, content: request.content }); + assert.match(getLastCallbackError() ?? '', /(unknown error|callback)/i); deregisterLlmSanitizeRequestGuardrail('node_llm_san_req_throw'); const result = await llmCallExecute( @@ -840,7 +867,7 @@ describe('LLM guardrails', () => { } }); - it('sanitize response guardrail failures omit the payload and remain usable', async () => { + it('sanitize response guardrail failures preserve the payload and remain usable', async () => { const events = []; clearLastCallbackError(); registerSubscriber('node_llm_san_resp_throw_sub', (event) => events.push(event)); @@ -850,7 +877,15 @@ describe('LLM guardrails', () => { try { const response = { ok: true }; await llmCallExecute('llm_san_resp_throw', makeNative(), () => response, null, null, null, null, null); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks(() => + events.some( + (event) => + event.name === 'llm_san_resp_throw' && + event.kind === 'scope' && + event.category === 'llm' && + event.scope_category === 'end', + ), + ); const end = events.find( (event) => event.name === 'llm_san_resp_throw' && @@ -858,7 +893,7 @@ describe('LLM guardrails', () => { event.category === 'llm' && event.scope_category === 'end', ); - assert.equal(end.data, null); + assert.deepEqual(end.data, response); assert.match(getLastCallbackError() ?? '', /response sanitizer boom/i); deregisterLlmSanitizeResponseGuardrail('node_llm_san_resp_throw'); @@ -885,6 +920,19 @@ describe('LLM guardrails', () => { deregisterLlmConditionalExecutionGuardrail('node_llm_cond'); }); + it('conditional guardrail awaits a Promise result', async () => { + registerLlmConditionalExecutionGuardrail('node_llm_cond_promise', 10, async () => { + await new Promise((resolve) => setImmediate(resolve)); + return null; + }); + try { + const result = await llmCallExecute('llm_cond_promise', makeNative(), () => ({ ok: true }), null, null, null, null, null); + assert.deepEqual(result, { ok: true }); + } finally { + deregisterLlmConditionalExecutionGuardrail('node_llm_cond_promise'); + } + }); + it('conditional guardrail treats implicit undefined as allow', async () => { registerLlmConditionalExecutionGuardrail('node_llm_cond_undefined', 10, () => undefined); try { @@ -1021,6 +1069,28 @@ describe('LLM intercepts', () => { deregisterLlmRequestIntercept('node_llm_req_mod'); }); + it('request intercept awaits a Promise result', async () => { + registerLlmRequestIntercept('node_llm_req_promise', 10, false, async ({ request, annotated }) => { + await new Promise((resolve) => setImmediate(resolve)); + return { request: { ...request, content: { ...request.content, promised: true } }, annotated }; + }); + try { + const result = await llmCallExecute( + 'llm_req_promise', + makeNative(), + (request) => ({ promised: request.content.promised }), + null, + null, + null, + null, + null, + ); + assert.deepEqual(result, { promised: true }); + } finally { + deregisterLlmRequestIntercept('node_llm_req_promise'); + } + }); + it('request intercept throws a catchable error without terminating Node', async () => { registerLlmRequestIntercept('node_llm_req_throw', 10, false, () => { throw new Error('llm request intercept boom'); @@ -1340,11 +1410,21 @@ describe('LLM intercepts', () => { deregisterLlmRequestIntercept('node_llm_req_helper'); }); - it('generated request-intercept declarations preserve the open optimization kind', () => { + it('generated request-intercept declarations reference the canonical open optimization type', () => { const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const pluginDeclarations = readFileSync(new URL('../plugin.d.ts', import.meta.url), 'utf8'); const openKind = "kind: 'input_compression' | 'model_routing' | (string & {})"; - assert.equal(declarations.split(openKind).length - 1, 3); + assert.equal(declarations.split(openKind).length - 1, 1); + assert.equal(pluginDeclarations.split(openKind).length - 1, 1); + assert.match( + declarations, + /registerLlmRequestIntercept\([^\n]*import\('\.\/plugin'\)\.LlmRequestInterceptOutcome/, + ); + assert.match( + declarations, + /scopeRegisterLlmRequestIntercept\([^\n]*import\('\.\/plugin'\)\.LlmRequestInterceptOutcome/, + ); }); it('generated LLM sanitizer declarations expose directional codec contexts', () => { diff --git a/crates/node/tests/scope_tests.mjs b/crates/node/tests/scope_tests.mjs index cb50f1ade..9ecd3ebb2 100644 --- a/crates/node/tests/scope_tests.mjs +++ b/crates/node/tests/scope_tests.mjs @@ -29,9 +29,13 @@ function rejectWithPrimitive(value) { return Promise.reject(value); } -async function flushSubscriberCallbacks() { +async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { await flushSubscribers(); - for (let i = 0; i < 10; i += 1) { + const deadline = Date.now() + timeoutMs; + while (!predicate()) { + if (Date.now() >= deadline) { + throw new Error('timed out waiting for subscriber callbacks'); + } await new Promise((resolve) => setImmediate(resolve)); } } @@ -104,7 +108,7 @@ describe('Scope operations', () => { try { const scope = pushScope('pop_metadata_scope', ScopeType.Agent, null, null, null, { a: 1, b: 2, c: 3 }); popScope(scope, null, null, { c: 3.5, d: 4 }); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks(() => events.some((e) => e.name === 'pop_metadata_scope' && e.scope_category === 'end')); const end = events.find( (e) => e.name === 'pop_metadata_scope' && e.kind === 'scope' && e.scope_category === 'end', @@ -208,7 +212,7 @@ describe('withScope', () => { await withScope('with_scope_ok_status', ScopeType.Function, () => ({ ok: true }), null, null, null, { caller: 'node', }); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks(() => events.some((e) => e.name === 'with_scope_ok_status' && e.scope_category === 'end')); const end = events.find( (e) => e.name === 'with_scope_ok_status' && e.kind === 'scope' && e.scope_category === 'end', @@ -260,7 +264,7 @@ describe('withScope', () => { }), /node status failure/, ); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks(() => events.some((e) => e.name === 'with_scope_error_status' && e.scope_category === 'end')); const end = events.find( (e) => e.name === 'with_scope_error_status' && e.kind === 'scope' && e.scope_category === 'end', @@ -355,8 +359,7 @@ describe('Subscribers', () => { try { const scope = pushScope('sub_test', ScopeType.Agent, null, null); popScope(scope); - await flushSubscriberCallbacks(); - assert.ok(events.length > 0, 'Expected at least one event'); + await waitForSubscriberCallbacks(() => events.length > 0); } finally { deregisterSubscriber('node_event_collector'); } @@ -367,8 +370,7 @@ describe('Subscribers', () => { registerSubscriber('node_flush_collector', (e) => events.push(e)); try { event('node_flush_mark', null, null, null); - await flushSubscribers(); - await new Promise((resolve) => setImmediate(resolve)); + await waitForSubscriberCallbacks(() => events.some((e) => e.kind === 'mark' && e.name === 'node_flush_mark')); assert.ok(events.some((e) => e.kind === 'mark' && e.name === 'node_flush_mark')); } finally { deregisterSubscriber('node_flush_collector'); @@ -383,7 +385,7 @@ describe('Subscribers', () => { try { const scope = pushScope('prop_test', ScopeType.Function, null, null); popScope(scope); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks(() => captured !== null); assert.ok(captured, 'Expected an event'); assert.ok(typeof captured.uuid === 'string'); assert.ok(typeof captured.timestamp === 'string'); @@ -406,7 +408,7 @@ describe('Subscribers', () => { }, null, ); - await flushSubscriberCallbacks(); + await waitForSubscriberCallbacks(() => events.some((e) => e.kind === 'mark')); const found = events.some((e) => e.kind === 'mark'); assert.ok(found, 'Expected a Mark event'); } finally { diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index 3d20628b6..b60642aef 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -608,6 +608,33 @@ describe('Tool guardrails', () => { deregisterToolConditionalExecutionGuardrail('node_tool_cond'); }); + it('conditional guardrail awaits a Promise result', async () => { + registerToolConditionalExecutionGuardrail('node_tool_cond_promise', 10, async () => { + await new Promise((resolve) => setImmediate(resolve)); + return null; + }); + try { + const result = await toolCallExecute('tool_cond_promise', { ok: true }, (args) => args, null, null, null, null); + assert.deepEqual(result, { ok: true }); + } finally { + deregisterToolConditionalExecutionGuardrail('node_tool_cond_promise'); + } + }); + + it('conditional guardrail propagates a rejected Promise', async () => { + registerToolConditionalExecutionGuardrail('node_tool_cond_reject', 10, async () => { + throw new Error('guardrail rejected promise'); + }); + try { + await assert.rejects( + () => toolCallExecute('tool_cond_reject', {}, () => ({ should_not: 'run' }), null, null, null, null), + /guardrail rejected promise/i, + ); + } finally { + deregisterToolConditionalExecutionGuardrail('node_tool_cond_reject'); + } + }); + it('conditional guardrail treats implicit undefined as allow', async () => { registerToolConditionalExecutionGuardrail('node_tool_cond_undefined', 10, () => undefined); try { @@ -782,6 +809,41 @@ describe('Tool intercepts', () => { deregisterToolRequestIntercept('node_tool_req_mod'); }); + it('request intercept awaits a Promise result', async () => { + registerToolRequestIntercept('node_tool_req_promise', 10, false, async (_name, args) => { + await new Promise((resolve) => setImmediate(resolve)); + return { ...args, promised: true }; + }); + try { + const result = await toolCallExecute( + 'tool_req_promise', + { original: true }, + (args) => args, + null, + null, + null, + null, + ); + assert.deepEqual(result, { original: true, promised: true }); + } finally { + deregisterToolRequestIntercept('node_tool_req_promise'); + } + }); + + it('request intercept propagates a rejected Promise', async () => { + registerToolRequestIntercept('node_tool_req_reject', 10, false, async () => { + throw new Error('request intercept rejected promise'); + }); + try { + await assert.rejects( + () => toolCallExecute('tool_req_reject', {}, () => ({ should_not: 'run' }), null, null, null, null), + /request intercept rejected promise/i, + ); + } finally { + deregisterToolRequestIntercept('node_tool_req_reject'); + } + }); + it('request intercept throws a catchable error without terminating Node', async () => { registerToolRequestIntercept('node_tool_req_throw', 10, false, () => { throw new Error('tool request intercept boom'); diff --git a/crates/pii-redaction/src/builtin.rs b/crates/pii-redaction/src/builtin.rs index 414fb13f6..71d421588 100644 --- a/crates/pii-redaction/src/builtin.rs +++ b/crates/pii-redaction/src/builtin.rs @@ -461,12 +461,15 @@ impl CompiledBuiltinBackend { } pub(super) fn tool_sanitize_callback(backend: CompiledBuiltinBackend) -> ToolSanitizeFn { - Arc::new( - move |_name: &str, payload: Json| match backend.trajectory.as_ref() { - Some(trajectory) => trajectory.sanitize_tool_payload(payload), - None => backend.sanitize_json_preorder_dfs(payload), - }, - ) + Arc::new(move |_name: String, payload: Json| { + let backend = backend.clone(); + Box::pin(async move { + Ok(match backend.trajectory.as_ref() { + Some(trajectory) => trajectory.sanitize_tool_payload(payload), + None => backend.sanitize_json_preorder_dfs(payload), + }) + }) + }) } pub(super) fn event_sanitize_callback(backend: CompiledBuiltinBackend) -> EventSanitizeFn { @@ -486,40 +489,43 @@ fn event_sanitize_callback_with_scope_categories( scope_categories: Option<(bool, bool)>, ) -> EventSanitizeFn { Arc::new(move |event, mut fields| { - if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { - matches!(event, Event::Scope(_)) + let backend = backend.clone(); + Box::pin(async move { + if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { + matches!(event, Event::Scope(_)) + && event + .category() + .is_some_and(|category| match category.as_str() { + "llm" => !sanitize_llm, + "tool" => !sanitize_tool, + _ => false, + }) + }) { + return Ok(fields); + } + + if let Some(trajectory) = backend.trajectory.as_ref() { + return Ok(trajectory.sanitize_event_fields(&event, fields)); + } + let specialized_scope = matches!(event, Event::Scope(_)) && event .category() - .is_some_and(|category| match category.as_str() { - "llm" => !sanitize_llm, - "tool" => !sanitize_tool, - _ => false, - }) - }) { - return fields; - } - - if let Some(trajectory) = backend.trajectory.as_ref() { - return trajectory.sanitize_event_fields(event, fields); - } - let specialized_scope = matches!(event, Event::Scope(_)) - && event - .category() - .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); - - if !specialized_scope { - fields.data = fields - .data - .map(|data| backend.sanitize_json_preorder_dfs(data)); - fields.category_profile = fields.category_profile.and_then(|profile| { - sanitize_serializable_with_backend::(&backend, profile).ok() - }); - } + .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); + + if !specialized_scope { + fields.data = fields + .data + .map(|data| backend.sanitize_json_preorder_dfs(data)); + fields.category_profile = fields.category_profile.and_then(|profile| { + sanitize_serializable_with_backend::(&backend, profile).ok() + }); + } - fields.metadata = fields - .metadata - .map(|metadata| backend.sanitize_json_preorder_dfs(metadata)); - fields + fields.metadata = fields + .metadata + .map(|metadata| backend.sanitize_json_preorder_dfs(metadata)); + Ok(fields) + }) }) } @@ -527,41 +533,44 @@ pub(super) fn llm_sanitize_request_callback( backend: CompiledBuiltinBackend, ) -> LlmSanitizeRequestFn { Arc::new(move |mut request: LlmRequest, context| { - if let Some(trajectory) = backend.trajectory.as_ref() { - request.headers = trajectory - .sanitize_tool_payload(Json::Object(request.headers)) - .as_object() - .cloned() - .unwrap_or_default(); - request.content = trajectory.sanitize_provider_payload(request.content); - return Some(request); - } - request.headers = backend.sanitize_request_headers(request.headers); - if backend.target_paths.is_empty() { - request.content = backend.sanitize_json_preorder_dfs(request.content); - return Some(request); - } - let resolved = context.resolve_codec(); - let fallback = if resolved.is_none() { - backend - .selected_surface(context.codec()) - .map(build_request_codec) - } else { - None - }; - let Some(codec) = resolved.as_deref().or(fallback.as_deref()) else { - log_llm_payload_omitted("request", context.codec(), "no usable request codec"); - return None; - }; - let sanitized = backend.sanitize_request_with_codec(codec, &request); - if sanitized.is_none() { - log_llm_payload_omitted( - "request", - context.codec(), - "codec decode, sanitize, or encode failure", - ); - } - sanitized + let backend = backend.clone(); + Box::pin(async move { + if let Some(trajectory) = backend.trajectory.as_ref() { + request.headers = trajectory + .sanitize_tool_payload(Json::Object(request.headers)) + .as_object() + .cloned() + .unwrap_or_default(); + request.content = trajectory.sanitize_provider_payload(request.content); + return Ok(Some(request)); + } + request.headers = backend.sanitize_request_headers(request.headers); + if backend.target_paths.is_empty() { + request.content = backend.sanitize_json_preorder_dfs(request.content); + return Ok(Some(request)); + } + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + backend + .selected_surface(context.codec()) + .map(build_request_codec) + } else { + None + }; + let Some(codec) = resolved.as_deref().or(fallback.as_deref()) else { + log_llm_payload_omitted("request", context.codec(), "no usable request codec"); + return Ok(None); + }; + let sanitized = backend.sanitize_request_with_codec(codec, &request); + if sanitized.is_none() { + log_llm_payload_omitted( + "request", + context.codec(), + "codec decode, sanitize, or encode failure", + ); + } + Ok(sanitized) + }) }) } @@ -569,49 +578,52 @@ pub(super) fn llm_sanitize_response_callback( backend: CompiledBuiltinBackend, ) -> LlmSanitizeResponseFn { Arc::new(move |payload: Json, context| { - if let Some(trajectory) = backend.trajectory.as_ref() { - return Some(trajectory.sanitize_provider_payload(payload)); - } - if backend.target_paths.is_empty() { - return Some(backend.sanitize_json_preorder_dfs(payload)); - } - if matches!(context.codec(), LlmCodecIdentity::None) - && !backend.uses_compatible_legacy_response_codec(&payload) - { - log_llm_payload_omitted( - "response", - context.codec(), - "no active response codec or compatible legacy codec", - ); - return None; - } - let Some(surface) = backend.selected_surface(context.codec()) else { - log_llm_payload_omitted( - "response", - context.codec(), - "no recognized response codec surface", - ); - return None; - }; - let resolved = context.resolve_codec(); - let fallback = if resolved.is_none() { - Some(build_response_codec(surface)) - } else { - None - }; - let Some(codec) = resolved.as_deref().or(fallback.as_deref()) else { - log_llm_payload_omitted("response", context.codec(), "no usable response codec"); - return None; - }; - let sanitized = backend.sanitize_response_with_codec(codec, surface, payload); - if sanitized.is_none() { - log_llm_payload_omitted( - "response", - context.codec(), - "codec decode, sanitize, or encode failure", - ); - } - sanitized + let backend = backend.clone(); + Box::pin(async move { + if let Some(trajectory) = backend.trajectory.as_ref() { + return Ok(Some(trajectory.sanitize_provider_payload(payload))); + } + if backend.target_paths.is_empty() { + return Ok(Some(backend.sanitize_json_preorder_dfs(payload))); + } + if matches!(context.codec(), LlmCodecIdentity::None) + && !backend.uses_compatible_legacy_response_codec(&payload) + { + log_llm_payload_omitted( + "response", + context.codec(), + "no active response codec or compatible legacy codec", + ); + return Ok(None); + } + let Some(surface) = backend.selected_surface(context.codec()) else { + log_llm_payload_omitted( + "response", + context.codec(), + "no recognized response codec surface", + ); + return Ok(None); + }; + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + Some(build_response_codec(surface)) + } else { + None + }; + let Some(codec) = resolved.as_deref().or(fallback.as_deref()) else { + log_llm_payload_omitted("response", context.codec(), "no usable response codec"); + return Ok(None); + }; + let sanitized = backend.sanitize_response_with_codec(codec, surface, payload); + if sanitized.is_none() { + log_llm_payload_omitted( + "response", + context.codec(), + "codec decode, sanitize, or encode failure", + ); + } + Ok(sanitized) + }) }) } diff --git a/crates/pii-redaction/tests/unit/component_tests.rs b/crates/pii-redaction/tests/unit/component_tests.rs index d4fe4e8be..2152c5d6d 100644 --- a/crates/pii-redaction/tests/unit/component_tests.rs +++ b/crates/pii-redaction/tests/unit/component_tests.rs @@ -296,8 +296,8 @@ impl LlmCodec for IdentifiedRequestCodec { } } -#[test] -fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs() { +#[tokio::test] +async fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "regex_replace".to_string(), @@ -324,7 +324,9 @@ fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs( }, LlmSanitizeRequestContext::for_request_codec(Some(Arc::new(OpenAIResponsesCodec))), ) - .expect("the active OpenAI Responses codec must override the legacy fallback"); + .await + .expect("the active OpenAI Responses codec must override the legacy fallback") + .expect("the active OpenAI Responses codec must retain the payload"); assert_eq!( active_request.content["input"][0]["content"][0]["text"], json!("[REDACTED]") @@ -350,7 +352,9 @@ fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs( inner: OpenAIResponsesCodec, }))), ) - .expect("an active runtime or opaque request codec must remain usable"); + .await + .expect("an active runtime or opaque request codec must remain usable") + .expect("an active runtime or opaque request codec must retain the payload"); assert_eq!( active_request.content["input"][0]["content"][0]["text"], json!("[REDACTED]") @@ -373,14 +377,19 @@ fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs( BuiltinLlmCodec::OpenAiResponses, )), ) - .expect("the active OpenAI Responses codec must override the legacy fallback"); + .await + .expect("the active OpenAI Responses codec must override the legacy fallback") + .expect("the active OpenAI Responses codec must retain the payload"); assert_eq!( active_responses["output"][0]["content"][0]["text"], json!("[REDACTED]") ); assert!( - sanitize_response(responses_payload.clone(), no_codec_context()).is_none(), + sanitize_response(responses_payload.clone(), no_codec_context()) + .await + .expect("sanitizer callback must succeed") + .is_none(), "an incompatible configured fallback codec must omit a normalized payload" ); @@ -389,6 +398,8 @@ fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs( responses_payload, LlmSanitizeResponseContext::with_identity(LlmCodecIdentity::Opaque), ) + .await + .expect("sanitizer callback must succeed") .is_none(), "a normalized-path policy must omit an unknown active provider payload" ); @@ -403,13 +414,15 @@ fn normalized_llm_paths_use_the_active_codec_and_fail_closed_for_unknown_codecs( "com.example.chat.v1".to_owned(), )), ) + .await + .expect("sanitizer callback must succeed") .is_none(), "a normalized-path policy must omit a runtime codec until it has a compatible projection" ); } -#[test] -fn normalized_llm_paths_omit_payloads_when_legacy_codec_decode_fails() { +#[tokio::test] +async fn normalized_llm_paths_omit_payloads_when_legacy_codec_decode_fails() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "regex_replace".to_string(), @@ -432,17 +445,22 @@ fn normalized_llm_paths_omit_payloads_when_legacy_codec_decode_fails() { }, no_codec_request_context(), ) + .await + .expect("sanitizer callback must succeed") .is_none(), "a shallow legacy surface match must not enable a raw-payload fallback" ); assert!( - sanitize_response(json!({"choices": "sk-response-secret"}), no_codec_context()).is_none(), + sanitize_response(json!({"choices": "sk-response-secret"}), no_codec_context()) + .await + .expect("sanitizer callback must succeed") + .is_none(), "a legacy response codec failure must omit the payload instead of emitting raw content" ); } -#[test] -fn normalized_openai_chat_api_specific_policy_omits_multiple_choices() { +#[tokio::test] +async fn normalized_openai_chat_api_specific_policy_omits_multiple_choices() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "remove".to_string(), @@ -475,12 +493,14 @@ fn normalized_openai_chat_api_specific_policy_omits_multiple_choices() { BuiltinLlmCodec::OpenAiChat, )), ) + .await + .expect("sanitizer callback must succeed") .is_none() ); } -#[test] -fn normalized_llm_paths_use_configured_anthropic_codec_without_a_system_message() { +#[tokio::test] +async fn normalized_llm_paths_use_configured_anthropic_codec_without_a_system_message() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "regex_replace".to_string(), @@ -504,7 +524,9 @@ fn normalized_llm_paths_use_configured_anthropic_codec_without_a_system_message( }, no_codec_request_context(), ) - .expect("the configured Anthropic codec must sanitize a valid message-only request"); + .await + .expect("the configured Anthropic codec must sanitize a valid message-only request") + .expect("the configured Anthropic codec must retain the payload"); assert_eq!( sanitized.content["messages"][0]["content"], @@ -512,8 +534,8 @@ fn normalized_llm_paths_use_configured_anthropic_codec_without_a_system_message( ); } -#[test] -fn trajectory_preset_redacts_chat_content_without_erasing_request_structure() { +#[tokio::test] +async fn trajectory_preset_redacts_chat_content_without_erasing_request_structure() { let callback = crate::builtin::llm_sanitize_request_callback(trajectory_backend( Some("openai_chat"), "preserve", @@ -548,6 +570,8 @@ fn trajectory_preset_redacts_chat_content_without_erasing_request_structure() { "person_name": "Alice Example" }), }, no_codec_request_context()) + .await + .unwrap() .unwrap(); assert_eq!(request.content["model"], "claude-sonnet-4-6"); @@ -595,8 +619,8 @@ fn trajectory_preset_redacts_chat_content_without_erasing_request_structure() { ); } -#[test] -fn trajectory_preset_preserves_response_analytics_and_redacts_response_content() { +#[tokio::test] +async fn trajectory_preset_preserves_response_analytics_and_redacts_response_content() { let callback = crate::builtin::llm_sanitize_response_callback(trajectory_backend( Some("openai_chat"), "preserve", @@ -617,6 +641,8 @@ fn trajectory_preset_preserves_response_analytics_and_redacts_response_content() }), no_codec_context(), ) + .await + .unwrap() .unwrap(); assert_eq!(sanitized["id"], "chatcmpl_1"); @@ -643,8 +669,8 @@ fn trajectory_preset_preserves_response_analytics_and_redacts_response_content() assert_eq!(sanitized["cost"]["total"], 1.25); } -#[test] -fn trajectory_preset_covers_responses_and_anthropic_provider_shapes() { +#[tokio::test] +async fn trajectory_preset_covers_responses_and_anthropic_provider_shapes() { let responses_request = crate::builtin::llm_sanitize_request_callback(trajectory_backend( Some("openai_responses"), "preserve", @@ -657,6 +683,8 @@ fn trajectory_preset_covers_responses_and_anthropic_provider_shapes() { "max_output_tokens": 100 }), }, no_codec_request_context()) + .await + .unwrap() .unwrap(); assert_eq!(responses_request.content["model"], "gpt-5"); assert_eq!(responses_request.content["input"][0]["role"], "user"); @@ -681,6 +709,8 @@ fn trajectory_preset_covers_responses_and_anthropic_provider_shapes() { }), no_codec_context(), ) + .await + .unwrap() .unwrap(); assert_eq!(responses_response["id"], "resp_1"); assert_eq!(responses_response["status"], "completed"); @@ -706,6 +736,8 @@ fn trajectory_preset_covers_responses_and_anthropic_provider_shapes() { "max_tokens": 128 }), }, no_codec_request_context()) + .await + .unwrap() .unwrap(); assert_eq!(anthropic_request.content["model"], "claude-sonnet-4-6"); assert_eq!(anthropic_request.content["system"], "[REDACTED]"); @@ -735,6 +767,8 @@ fn trajectory_preset_covers_responses_and_anthropic_provider_shapes() { "stop_reason": "end_turn", "usage": {"input_tokens": 12, "output_tokens": 6, "cache_read_input_tokens": 8} }), no_codec_context()) + .await + .unwrap() .unwrap(); assert_eq!(anthropic_response["id"], "msg_1"); assert_eq!(anthropic_response["role"], "assistant"); @@ -745,8 +779,8 @@ fn trajectory_preset_covers_responses_and_anthropic_provider_shapes() { assert_eq!(anthropic_response["usage"]["cache_read_input_tokens"], 8); } -#[test] -fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { +#[tokio::test] +async fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { let callback = crate::builtin::event_sanitize_callback(trajectory_backend(None, "preserve")); let chunk = Event::Mark(MarkEvent::new( BaseEvent::builder().name("llm.chunk").build(), @@ -754,7 +788,7 @@ fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { Some(CategoryProfile::builder().subtype("llm.chunk").build()), )); let sanitized = callback( - &chunk, + chunk.clone(), EventSanitizeFields { data: Some(json!({ "chunk_index": 2, @@ -764,7 +798,9 @@ fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { category_profile: chunk.category_profile().cloned(), metadata: None, }, - ); + ) + .await + .unwrap(); assert_eq!(sanitized.data.as_ref().unwrap()["chunk_index"], 2); assert_eq!( sanitized.data.as_ref().unwrap()["event_type"], @@ -787,7 +823,7 @@ fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { ), )); let sanitized = callback( - &optimization, + optimization.clone(), EventSanitizeFields { data: Some(json!({ "producer": "neutral.router", @@ -803,7 +839,9 @@ fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { category_profile: optimization.category_profile().cloned(), metadata: None, }, - ); + ) + .await + .unwrap(); assert_eq!( sanitized.data.as_ref().unwrap()["producer"], "neutral.router" @@ -829,7 +867,7 @@ fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { None, )); let sanitized = callback( - &nested_agent, + nested_agent, EventSanitizeFields { data: Some(json!({ "request_id": "request-1", @@ -839,7 +877,9 @@ fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { category_profile: None, metadata: Some(json!({"parent_scope_id": "scope-1", "note": "private note"})), }, - ); + ) + .await + .unwrap(); assert_eq!(sanitized.data.as_ref().unwrap()["request_id"], "request-1"); assert_eq!( sanitized.data.as_ref().unwrap()["instruction"], @@ -860,8 +900,8 @@ fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { assert_eq!(sanitized.metadata.as_ref().unwrap()["note"], "[REDACTED]"); } -#[test] -fn trajectory_preset_preserves_trusted_scope_metadata_only() { +#[tokio::test] +async fn trajectory_preset_preserves_trusted_scope_metadata_only() { let callback = crate::builtin::event_sanitize_callback(trajectory_backend(None, "preserve")); let metadata = json!({ "nemo_relay_scope_role": "turn", @@ -922,13 +962,15 @@ fn trajectory_preset_preserves_trusted_scope_metadata_only() { None, )); let sanitized = callback( - &event, + event, EventSanitizeFields { data: None, category_profile: None, metadata: Some(metadata.clone()), }, - ); + ) + .await + .unwrap(); assert_eq!(sanitized.metadata, Some(expected_metadata.clone())); } @@ -940,7 +982,7 @@ fn trajectory_preset_preserves_trusted_scope_metadata_only() { None, )); let sanitized = callback( - &malformed, + malformed, EventSanitizeFields { data: None, category_profile: None, @@ -951,7 +993,9 @@ fn trajectory_preset_preserves_trusted_scope_metadata_only() { "provider_payload_exact": "private context" })), }, - ); + ) + .await + .unwrap(); assert_eq!( sanitized.metadata, Some(json!({ @@ -968,21 +1012,23 @@ fn trajectory_preset_preserves_trusted_scope_metadata_only() { Some(CategoryProfile::builder().subtype("llm.chunk").build()), )); let sanitized = callback( - &mark, + mark.clone(), EventSanitizeFields { data: None, category_profile: mark.category_profile().cloned(), metadata: Some(json!({"harness": "codex", "source": "hook"})), }, - ); + ) + .await + .unwrap(); assert_eq!( sanitized.metadata, Some(json!({"harness": "[REDACTED]", "source": "[REDACTED]"})) ); } -#[test] -fn trajectory_custom_mark_policy_is_explicit_and_shape_preserving() { +#[tokio::test] +async fn trajectory_custom_mark_policy_is_explicit_and_shape_preserving() { let event = Event::Mark(MarkEvent::new( BaseEvent::builder().name("neutral.plugin.evidence").build(), Some(EventCategory::custom()), @@ -999,11 +1045,14 @@ fn trajectory_custom_mark_policy_is_explicit_and_shape_preserving() { }; let preserve = crate::builtin::event_sanitize_callback(trajectory_backend(None, "preserve")); - assert_eq!(preserve(&event, fields.clone()), fields); + assert_eq!( + preserve(event.clone(), fields.clone()).await.unwrap(), + fields + ); let redact = crate::builtin::event_sanitize_callback(trajectory_backend(None, "redact_all_leaves")); - let sanitized = redact(&event, fields); + let sanitized = redact(event, fields).await.unwrap(); assert_eq!( sanitized.data.unwrap(), json!({ @@ -1016,8 +1065,8 @@ fn trajectory_custom_mark_policy_is_explicit_and_shape_preserving() { assert_eq!(profile.extra["opaque"]["label"], "[REDACTED]"); } -#[test] -fn trajectory_profile_preserves_typed_llm_accounting_while_redacting_annotations() { +#[tokio::test] +async fn trajectory_profile_preserves_typed_llm_accounting_while_redacting_annotations() { let callback = crate::builtin::event_sanitize_callback(trajectory_backend(None, "preserve")); let annotated_response: nemo_relay::codec::response::AnnotatedLlmResponse = serde_json::from_value(json!({ @@ -1068,7 +1117,7 @@ fn trajectory_profile_preserves_typed_llm_accounting_while_redacting_annotations None, )); let sanitized = callback( - &event, + event, EventSanitizeFields { data: Some(json!({"already": "sanitized by the response callback"})), category_profile: Some( @@ -1079,7 +1128,9 @@ fn trajectory_profile_preserves_typed_llm_accounting_while_redacting_annotations ), metadata: None, }, - ); + ) + .await + .unwrap(); let profile = sanitized.category_profile.unwrap(); assert_eq!(profile.model_name.as_deref(), Some("claude-sonnet-4-6")); @@ -1116,8 +1167,8 @@ fn trajectory_profile_preserves_typed_llm_accounting_while_redacting_annotations ); } -#[test] -fn preserved_custom_marks_remain_eligible_for_a_later_email_profile() { +#[tokio::test] +async fn preserved_custom_marks_remain_eligible_for_a_later_email_profile() { let event = Event::Mark(MarkEvent::new( BaseEvent::builder().name("neutral.plugin.evidence").build(), Some(EventCategory::custom()), @@ -1141,7 +1192,8 @@ fn preserved_custom_marks_remain_eligible_for_a_later_email_profile() { .unwrap(), ); - let sanitized = email(&event, trajectory(&event, fields)); + let fields = trajectory(event.clone(), fields).await.unwrap(); + let sanitized = email(event, fields).await.unwrap(); assert_eq!(sanitized.data.as_ref().unwrap()["owner"], "[REDACTED]"); assert_eq!(sanitized.data.as_ref().unwrap()["score"], 0.9); assert_eq!( @@ -1365,7 +1417,11 @@ fn local_profile_registrations_receive_generated_namespaces() { reset_runtime(); register_local_backend_provider(Arc::new(|_, ctx| { - ctx.register_mark_sanitize_guardrail("shared", 100, Arc::new(|_, fields| fields)) + ctx.register_mark_sanitize_guardrail( + "shared", + 100, + Arc::new(|_, fields| Box::pin(async move { Ok(fields) })), + ) })) .unwrap(); @@ -1430,8 +1486,8 @@ fn failed_later_profile_rolls_back_earlier_profile_registrations() { deregister_subscriber("pii-profile-rollback").unwrap(); } -#[test] -fn event_sanitizer_transforms_data_category_profile_and_metadata_independently() { +#[tokio::test] +async fn event_sanitizer_transforms_data_category_profile_and_metadata_independently() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "regex_replace".into(), @@ -1449,7 +1505,7 @@ fn event_sanitizer_transforms_data_category_profile_and_metadata_independently() None, )); let sanitized = callback( - &event, + event, EventSanitizeFields { data: Some(json!({"email": "person@example.com"})), category_profile: Some( @@ -1459,7 +1515,9 @@ fn event_sanitizer_transforms_data_category_profile_and_metadata_independently() ), metadata: Some(json!({"owner": "person@example.com"})), }, - ); + ) + .await + .unwrap(); assert_eq!(sanitized.data.unwrap()["email"], "[REDACTED]"); assert_eq!( sanitized.category_profile.unwrap().subtype.as_deref(), @@ -1468,8 +1526,8 @@ fn event_sanitizer_transforms_data_category_profile_and_metadata_independently() assert_eq!(sanitized.metadata.unwrap()["owner"], "[REDACTED]"); } -#[test] -fn llm_and_tool_scope_metadata_is_sanitized_without_reprocessing_typed_fields() { +#[tokio::test] +async fn llm_and_tool_scope_metadata_is_sanitized_without_reprocessing_typed_fields() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "redact".into(), @@ -1493,13 +1551,15 @@ fn llm_and_tool_scope_metadata_is_sanitized_without_reprocessing_typed_fields() .subtype("person@example.com") .build(); let sanitized = callback( - &event, + event, EventSanitizeFields { data: Some(json!({"content": "person@example.com"})), category_profile: Some(original_profile.clone()), metadata: Some(json!({"owner": "person@example.com"})), }, - ); + ) + .await + .unwrap(); assert_eq!( sanitized.data.unwrap()["content"], @@ -1511,8 +1571,8 @@ fn llm_and_tool_scope_metadata_is_sanitized_without_reprocessing_typed_fields() } } -#[test] -fn scope_event_sanitizer_respects_enabled_llm_and_tool_surfaces() { +#[tokio::test] +async fn scope_event_sanitizer_respects_enabled_llm_and_tool_surfaces() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "redact".into(), @@ -1547,13 +1607,15 @@ fn scope_event_sanitizer_respects_enabled_llm_and_tool_surfaces() { .subtype("person@example.com") .build(); let sanitized = callback( - &event, + event, EventSanitizeFields { data: Some(json!({"content": "person@example.com"})), category_profile: Some(original_profile.clone()), metadata: Some(json!({"owner": "person@example.com"})), }, - ); + ) + .await + .unwrap(); assert_eq!(sanitized.data.unwrap()["content"], "person@example.com"); assert_eq!(sanitized.category_profile.unwrap(), original_profile); @@ -1562,8 +1624,8 @@ fn scope_event_sanitizer_respects_enabled_llm_and_tool_surfaces() { } } -#[test] -fn event_sanitizer_discards_category_profile_when_sanitization_fails() { +#[tokio::test] +async fn event_sanitizer_discards_category_profile_when_sanitization_fails() { let backend = crate::builtin::CompiledBuiltinBackend::new( BuiltinBackendConfig { action: "regex_replace".into(), @@ -1581,7 +1643,7 @@ fn event_sanitizer_discards_category_profile_when_sanitization_fails() { None, )); let sanitized = callback( - &event, + event, EventSanitizeFields { data: None, category_profile: Some(CategoryProfile { @@ -1593,7 +1655,9 @@ fn event_sanitizer_discards_category_profile_when_sanitization_fails() { }), metadata: None, }, - ); + ) + .await + .unwrap(); assert!(sanitized.category_profile.is_none()); } diff --git a/crates/plugin/README.md b/crates/plugin/README.md index 7f3dd6c42..0b68a888e 100644 --- a/crates/plugin/README.md +++ b/crates/plugin/README.md @@ -42,8 +42,12 @@ the dynamic-library boundary on the stable C-compatible ABI. - **`PluginContext`**: Component-scoped registration APIs for middleware and subscribers. - **`PluginRuntime`**: Typed helpers for Relay-owned scopes and marks. -- **Stable native ABI v1**: C-compatible host and plugin tables behind the - safe Rust authoring interface. +- **Stable native ABI v3**: C-compatible host and plugin tables behind the + safe Rust authoring interface, with a v2-compatible prefix for existing + plugins. +- **Raw async middleware**: Completion-based raw registrations for plugins + that need asynchronous guardrails, intercepts, or event sanitizers. Typed + Rust callbacks remain synchronous convenience APIs. ## Installation diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 727e09aa2..5f5500072 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -36,7 +36,15 @@ use serde::{Serialize, de::DeserializeOwned}; use serde_json::Map; /// Native plugin ABI version supported by this crate. -pub const NEMO_RELAY_NATIVE_ABI_VERSION: u32 = 2; +/// +/// Version 3 reserves the native async middleware extension. Hosts retain a +/// version-2 table for already-built plugins during entry-point negotiation. +pub const NEMO_RELAY_NATIVE_ABI_VERSION: u32 = 3; +/// ABI version that introduced completion-based asynchronous middleware. +pub const NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE: u32 = 3; + +/// Legacy native plugin ABI accepted by Relay hosts for compatibility. +pub const NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY: u32 = 2; /// Built-in LLM codec identities available to native plugins. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)] @@ -754,6 +762,133 @@ pub struct NemoRelayNativeHostApiV1 { ) -> NemoRelayStatus, } +/// Middleware surface selected by the native async registration hook. +/// +/// The host only exposes this through the ABI-v3 extension table. It keeps +/// every asynchronous callback shape uniform while allowing the host to +/// deserialize the surface-specific invocation and result payloads. +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NemoRelayNativeAsyncMiddlewareKind { + /// Tool start-event request sanitizer. + ToolSanitizeRequest = 0, + /// Tool end-event response sanitizer. + ToolSanitizeResponse = 1, + /// Tool execution admission guardrail. + ToolConditionalExecution = 2, + /// Tool request rewrite intercept. + ToolRequestIntercept = 3, + /// Tool execution intercept with a continuation. + ToolExecutionIntercept = 4, + /// LLM start-event request sanitizer. + LlmSanitizeRequest = 5, + /// LLM end-event response sanitizer. + LlmSanitizeResponse = 6, + /// LLM execution admission guardrail. + LlmConditionalExecution = 7, + /// LLM request rewrite intercept. + LlmRequestIntercept = 8, + /// LLM execution intercept with a continuation. + LlmExecutionIntercept = 9, + /// Streaming LLM execution intercept with a continuation. + LlmStreamExecutionIntercept = 10, + /// Mark event sanitizer. + MarkSanitize = 11, + /// Scope-start event sanitizer. + ScopeSanitizeStart = 12, + /// Scope-end event sanitizer. + ScopeSanitizeEnd = 13, +} + +/// Indicates whether an asynchronous native callback settled before returning. +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NemoRelayNativeAsyncCallbackState { + /// The callback settled its completion before returning. + Complete = 0, + /// The callback retained its completion for later settlement. + Pending = 1, +} + +/// Opaque one-shot completion retained by a pending native callback. +#[repr(C)] +pub struct NemoRelayNativeAsyncCompletion { + _private: [u8; 0], + _marker: PhantomData<(*mut u8, PhantomPinned)>, +} + +/// Opaque native execution continuation supplied only to execution intercepts. +#[repr(C)] +pub struct NemoRelayNativeAsyncNext { + _private: [u8; 0], + _marker: PhantomData<(*mut u8, PhantomPinned)>, +} + +/// Completion-based native middleware callback. +/// +/// `invocation_json` is borrowed for the call. A callback that returns +/// [`NemoRelayNativeAsyncCallbackState::Pending`] owns one completion +/// reference and must settle it then call the v3 `async_completion_release` +/// hook. When `next` is non-null, the callback owns that handle for the +/// invocation and must call `async_next_release` after its final use. `next` +/// is null for non-execution middleware. +pub type NemoRelayNativeAsyncMiddlewareCb = + unsafe extern "C" fn( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + next: *const NemoRelayNativeAsyncNext, + completion: *const NemoRelayNativeAsyncCompletion, + ) -> NemoRelayNativeAsyncCallbackState; + +/// ABI-v3 host extension appended to [`NemoRelayNativeHostApiV1`]. +/// +/// Its first field is the complete v1/v2 table, so legacy plugins can keep +/// treating the pointer as a [`NemoRelayNativeHostApiV1`]. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct NemoRelayNativeHostApiV3 { + /// Compatibility prefix for ABI-v1/v2 plugins. + pub v1: NemoRelayNativeHostApiV1, + /// Resolves an async callback completion with a JSON value. + pub async_completion_resolve_json: unsafe extern "C" fn( + completion: *const NemoRelayNativeAsyncCompletion, + value_json: *const NemoRelayNativeString, + ) -> NemoRelayStatus, + /// Rejects an async callback completion with a UTF-8 message. + pub async_completion_reject: unsafe extern "C" fn( + completion: *const NemoRelayNativeAsyncCompletion, + message: *const NemoRelayNativeString, + ) -> NemoRelayStatus, + /// Returns true after the awaiting runtime has cancelled the invocation. + pub async_completion_is_cancelled: + unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion) -> bool, + /// Releases the callback-owned reference after a pending completion settles. + pub async_completion_release: + unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion), + /// Invokes an execution continuation and settles a supplied completion. + pub async_next_invoke: unsafe extern "C" fn( + next: *const NemoRelayNativeAsyncNext, + invocation_json: *const NemoRelayNativeString, + completion: *const NemoRelayNativeAsyncCompletion, + ) -> NemoRelayStatus, + /// Releases the callback-owned continuation reference for a pending callback. + pub async_next_release: unsafe extern "C" fn(next: *const NemoRelayNativeAsyncNext), + /// Registers any completion-based asynchronous middleware surface. + pub plugin_context_register_async_middleware: unsafe extern "C" fn( + ctx: *mut NemoRelayNativePluginContext, + kind: NemoRelayNativeAsyncMiddlewareKind, + name: *const NemoRelayNativeString, + priority: i32, + break_chain: bool, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, + ) -> NemoRelayStatus, +} + +unsafe impl Send for NemoRelayNativeHostApiV3 {} +unsafe impl Sync for NemoRelayNativeHostApiV3 {} + // The host API table is immutable after construction. Function pointers and // the null-terminated version string pointer are safe to share across threads. unsafe impl Send for NemoRelayNativeHostApiV1 {} @@ -2231,6 +2366,47 @@ impl<'a> PluginContext<'a> { }) } + /// Registers completion-based asynchronous middleware through the ABI-v3 + /// extension table. + /// + /// Plugins built against older hosts receive [`NemoRelayStatus::InvalidArg`] + /// instead of attempting to read beyond the legacy host table. + /// + /// # Safety + /// `cb`, `user_data`, and `free_fn` must remain valid until the host + /// deregisters the callback or invokes `free_fn`. A callback returning + /// `Pending` must settle and release its completion/next references. + #[allow(clippy::too_many_arguments)] // Mirrors the native C ABI registration callback. + pub unsafe fn register_async_middleware_raw( + &mut self, + kind: NemoRelayNativeAsyncMiddlewareKind, + name: &str, + priority: i32, + break_chain: bool, + cb: NemoRelayNativeAsyncMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, + ) -> NemoRelayStatus { + if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE + || self.host.struct_size < std::mem::size_of::() + { + return NemoRelayStatus::InvalidArg; + } + let host = unsafe { &*(self.host as *const _ as *const NemoRelayNativeHostApiV3) }; + self.with_name(name, |_, name| unsafe { + (host.plugin_context_register_async_middleware)( + self.raw, + kind, + name, + priority, + break_chain, + cb, + user_data, + free_fn, + ) + }) + } + fn with_name( &self, name: &str, diff --git a/crates/plugin/tests/typed_callbacks.rs b/crates/plugin/tests/typed_callbacks.rs index fb3562bd3..865f05e16 100644 --- a/crates/plugin/tests/typed_callbacks.rs +++ b/crates/plugin/tests/typed_callbacks.rs @@ -300,8 +300,8 @@ static LLM_REQUEST_INTERCEPT_REGISTRATION: Mutex(), test_host().struct_size diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index f5c2ccd51..80b9d672e 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1313,12 +1313,42 @@ fn deregister_llm_stream_execution_intercept(name: &str) -> PyResult { #[pyfunction] fn tool_request_intercepts<'py>( py: Python<'py>, - name: &str, + name: String, args: &Bound<'py, PyAny>, -) -> PyResult> { +) -> PyResult> { let args_json = py_to_json(args)?; - let result = core_tool_api::tool_request_intercepts(name, args_json).map_err(to_py_err)?; - json_to_py(py, &result) + // Preserve the established synchronous helper behavior when no Python + // event loop is active. Awaitable middleware is supported from async + // callers below; a synchronous caller can continue using direct + // callbacks without manufacturing an asyncio loop. + if py + .import("asyncio")? + .call_method0("get_running_loop") + .is_err() + { + let scope_stack = current_scope_stack_handle(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; + let result = runtime + .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { + core_tool_api::tool_request_intercepts(&name, args_json).await + })) + .map_err(to_py_err)?; + return json_to_py(py, &result).map(|value| value.into_bound(py)); + } + let scope_stack = current_scope_stack_handle(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let result = core_tool_api::tool_request_intercepts(&name, args_json) + .await + .map_err(to_py_err)?; + Python::attach(|py| json_to_py(py, &result)) + }) + .await + }) } /// Run the registered tool conditional execution guardrail chain. @@ -1329,9 +1359,39 @@ fn tool_request_intercepts<'py>( /// name: Tool name. /// args: Tool arguments (any JSON-serializable object). #[pyfunction] -fn tool_conditional_execution(name: &str, args: &Bound<'_, PyAny>) -> PyResult<()> { +fn tool_conditional_execution<'py>( + py: Python<'py>, + name: String, + args: &Bound<'py, PyAny>, +) -> PyResult> { let args_json = py_to_json(args)?; - core_tool_api::tool_conditional_execution(name, &args_json).map_err(to_py_err) + if py + .import("asyncio")? + .call_method0("get_running_loop") + .is_err() + { + let scope_stack = current_scope_stack_handle(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; + runtime + .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { + core_tool_api::tool_conditional_execution(&name, &args_json).await + })) + .map_err(to_py_err)?; + return Ok(py.None().into_bound(py)); + } + let scope_stack = current_scope_stack_handle(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_conditional_execution(&name, &args_json) + .await + .map_err(to_py_err) + }) + .await + }) } /// Run the registered LLM request intercept chain on the given request. @@ -1344,12 +1404,43 @@ fn tool_conditional_execution(name: &str, args: &Bound<'_, PyAny>) -> PyResult<( /// Returns: /// The (possibly transformed) ``LlmRequest``. #[pyfunction] -fn llm_request_intercepts( - name: &str, +fn llm_request_intercepts<'py>( + py: Python<'py>, + name: String, request: PyLLMRequest, -) -> PyResult { - let result = core_llm_api::llm_request_intercepts(name, request.inner).map_err(to_py_err)?; - Ok(crate::py_types::PyLLMRequestInterceptOutcome { inner: result }) +) -> PyResult> { + if py + .import("asyncio")? + .call_method0("get_running_loop") + .is_err() + { + let scope_stack = current_scope_stack_handle(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; + let result = runtime + .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { + core_llm_api::llm_request_intercepts(&name, request.inner).await + })) + .map_err(to_py_err)?; + return Py::new( + py, + crate::py_types::PyLLMRequestInterceptOutcome { inner: result }, + ) + .map(|value| value.into_bound(py).into_any()); + } + let scope_stack = current_scope_stack_handle(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let result = core_llm_api::llm_request_intercepts(&name, request.inner) + .await + .map_err(to_py_err)?; + Ok(crate::py_types::PyLLMRequestInterceptOutcome { inner: result }) + }) + .await + }) } /// Run the registered LLM conditional execution guardrail chain. @@ -1359,8 +1450,37 @@ fn llm_request_intercepts( /// Args: /// request: An ``LlmRequest`` object. #[pyfunction] -fn llm_conditional_execution(request: PyLLMRequest) -> PyResult<()> { - core_llm_api::llm_conditional_execution(&request.inner).map_err(to_py_err) +fn llm_conditional_execution<'py>( + py: Python<'py>, + request: PyLLMRequest, +) -> PyResult> { + if py + .import("asyncio")? + .call_method0("get_running_loop") + .is_err() + { + let scope_stack = current_scope_stack_handle(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; + runtime + .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { + core_llm_api::llm_conditional_execution(&request.inner).await + })) + .map_err(to_py_err)?; + return Ok(py.None().into_bound(py)); + } + let scope_stack = current_scope_stack_handle(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_llm_api::llm_conditional_execution(&request.inner) + .await + .map_err(to_py_err) + }) + .await + }) } // --------------------------------------------------------------------------- diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 57fffd5d5..f5cb02e55 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -39,7 +39,7 @@ use tokio_stream::Stream; use tokio_stream::wrappers::ReceiverStream; use nemo_relay::api::event::{Event, EventSanitizeFields}; -use nemo_relay::api::llm::{LlmRequest, LlmRequestInterceptOutcome}; +use nemo_relay::api::llm::LlmRequest; use nemo_relay::api::tool::ToolExecutionInterceptOutcome; use nemo_relay::codec::request::AnnotatedLlmRequest as AnnotatedLLMRequest; use nemo_relay::codec::response::AnnotatedLlmResponse as AnnotatedLLMResponse; @@ -408,71 +408,69 @@ fn stream_from_async_iter(async_iter: Py) -> FlowResult { /// Wrap a Python callable `(str, Json) -> Json` for tool sanitize/intercept fns. pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { - Arc::new(move |name: &str, args: Json| { - Python::attach(|py| { - let py_args = match json_to_py(py, &args) { - Ok(v) => v, - Err(e) => { - eprintln!("nemo_relay: json_to_py failed in tool fn for '{name}': {e}"); - return args.clone(); - } - }; - let result = match py_fn.call1(py, (name, py_args)) { - Ok(v) => v, - Err(e) => { - eprintln!("nemo_relay: Python tool callable failed for '{name}': {e}"); - return args.clone(); - } - }; - py_to_json(result.bind(py)).unwrap_or_else(|e| { - eprintln!("nemo_relay: py_to_json failed in tool fn for '{name}': {e}"); - args.clone() - }) + let py_fn = Arc::new(py_fn); + Arc::new(move |name: String, args: Json| { + let py_fn = py_fn.clone(); + Box::pin(async move { + resolve_json_or_future(Python::attach(|py| { + let py_args = json_to_py(py, &args) + .map_err(|e| FlowError::Internal(format!("tool json_to_py failed: {e}")))?; + let result = py_fn.call1(py, (name, py_args)).map_err(|e| { + FlowError::Internal(format!("Python tool callback failed: {e}")) + })?; + split_json_or_future(py, result) + })) + .await }) }) } /// Wrap a Python callable `(str, Json) -> Optional[str]` for tool conditional guardrails. pub fn wrap_py_tool_conditional_fn(py_fn: Py) -> ToolConditionalFn { - Arc::new(move |name: &str, args: &Json| { - Python::attach(|py| { - let py_args = json_to_py(py, args).map_err(|e| { - FlowError::Internal(format!( - "tool conditional json_to_py failed for '{name}': {e}" - )) - })?; - let result = py_fn.call1(py, (name, py_args)).map_err(|e| { - FlowError::Internal(format!( - "Python tool conditional callable failed for '{name}': {e}" - )) - })?; - let bound = result.bind(py); - if bound.is_none() { - Ok(None) - } else { - bound.extract::().map(Some).map_err(|e| { - FlowError::Internal(format!( - "tool conditional guardrail for '{name}' returned unexpected type (expected str or None): {e}" - )) - }) - } + let py_fn = Arc::new(py_fn); + Arc::new(move |name: String, args: Json| { + let py_fn = py_fn.clone(); + Box::pin(async move { + let result = resolve_py_object_or_future(Python::attach(|py| { + let py_args = + json_to_py(py, &args).map_err(|e| FlowError::Internal(e.to_string()))?; + let result = py_fn + .call1(py, (name, py_args)) + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_py_object_or_future(py, result) + })) + .await?; + Python::attach(|py| { + let bound = result.bind(py); + if bound.is_none() { + Ok(None) + } else { + bound.extract::().map(Some).map_err(|e| { + FlowError::Internal(format!( + "tool conditional guardrail returned unexpected type: {e}" + )) + }) + } + }) }) }) } /// Wrap a Python callable `(str, Json) -> Json` for tool request intercepts. pub fn wrap_py_tool_request_intercept_fn(py_fn: Py) -> ToolInterceptFn { - Arc::new(move |name: &str, args: Json| { - Python::attach(|py| { - let py_args = json_to_py(py, &args).map_err(|e| { - FlowError::Internal(format!("tool callback json_to_py failed for '{name}': {e}")) - })?; - let result = py_fn.call1(py, (name, py_args)).map_err(|e| { - FlowError::Internal(format!("Python tool callable failed for '{name}': {e}")) - })?; - py_to_json(result.bind(py)).map_err(|e| { - FlowError::Internal(format!("tool callback py_to_json failed for '{name}': {e}")) - }) + let py_fn = Arc::new(py_fn); + Arc::new(move |name: String, args: Json| { + let py_fn = py_fn.clone(); + Box::pin(async move { + resolve_json_or_future(Python::attach(|py| { + let py_args = + json_to_py(py, &args).map_err(|e| FlowError::Internal(e.to_string()))?; + let result = py_fn + .call1(py, (name, py_args)) + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_json_or_future(py, result) + })) + .await }) }) } @@ -815,30 +813,38 @@ pub fn wrap_py_llm_stream_exec_intercept_fn( /// Wrap a Python callable `(LlmRequest, LlmSanitizeRequestContext) -> Optional`. fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequestFn { + let py_fn = Arc::new(py_fn); Arc::new( move |request: LlmRequest, context: LlmSanitizeRequestContext| { - Python::attach(|py| { - let py_context = PyLlmSanitizeRequestContext { inner: context }; - let py_request = PyLLMRequest { inner: request }; - let result = match py_fn.call1(py, (py_request, py_context)) { - Ok(value) => value, - Err(error) => { - eprintln!("nemo_relay: LLM sanitize request callable failed: {error}"); - return None; - } - }; - if result.is_none(py) { - return None; - } - match result.extract::(py) { - Ok(request) => Some(request.inner), - Err(error) => { - eprintln!( - "nemo_relay: LLM sanitize request callable returned unexpected type: {error}" - ); - None + let py_fn = py_fn.clone(); + Box::pin(async move { + let result = resolve_py_object_or_future(Python::attach(|py| { + let result = py_fn + .call1( + py, + ( + PyLLMRequest { inner: request }, + PyLlmSanitizeRequestContext { inner: context }, + ), + ) + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_py_object_or_future(py, result) + })) + .await?; + Python::attach(|py| { + if result.is_none(py) { + Ok(None) + } else { + result + .extract::(py) + .map(|request| Some(request.inner)) + .map_err(|error| { + FlowError::Internal(format!( + "LLM sanitize request returned unexpected type: {error}" + )) + }) } - } + }) }) }, ) @@ -846,24 +852,29 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest /// Wrap a Python callable `(LlmRequest) -> Optional[str]` for LLM conditional guardrails. pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { - Arc::new(move |request: &LlmRequest| { - Python::attach(|py| { - let py_req = PyLLMRequest { - inner: request.clone(), - }; - let result = py_fn.call1(py, (py_req,)).map_err(|e| { - FlowError::Internal(format!("LLM conditional guardrail callable failed: {e}")) - })?; - let bound = result.bind(py); - if bound.is_none() { - Ok(None) - } else { - bound.extract::().map(Some).map_err(|e| { - FlowError::Internal(format!( - "LLM conditional guardrail returned unexpected type (expected str or None): {e}" - )) - }) - } + let py_fn = Arc::new(py_fn); + Arc::new(move |request: LlmRequest| { + let py_fn = py_fn.clone(); + Box::pin(async move { + let result = resolve_py_object_or_future(Python::attach(|py| { + let result = py_fn + .call1(py, (PyLLMRequest { inner: request },)) + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_py_object_or_future(py, result) + })) + .await?; + Python::attach(|py| { + let bound = result.bind(py); + if bound.is_none() { + Ok(None) + } else { + bound.extract::().map(Some).map_err(|e| { + FlowError::Internal(format!( + "LLM conditional guardrail returned unexpected type: {e}" + )) + }) + } + }) }) }) } @@ -875,42 +886,45 @@ pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { /// When ``annotated`` is present, request content is read-only and provider-body /// edits must be made through the returned annotation; headers remain writable. pub fn wrap_py_llm_request_intercept_fn(py_fn: Py) -> LlmRequestInterceptFn { + let py_fn = Arc::new(py_fn); Arc::new( - move |name: &str, - request: LlmRequest, - annotated: Option| - -> FlowResult { - Python::attach(|py| { - let py_req = PyLLMRequest { - inner: request.clone(), - }; - let py_ann: Py = match annotated { - Some(ann) => { - let wrapper = PyAnnotatedLLMRequest { inner: ann }; - wrapper - .into_pyobject(py) - .map_err(|e| { - FlowError::Internal(format!( - "Failed to convert AnnotatedLLMRequest to Python: {e}" - )) - })? - .into_any() - .unbind() - } - None => py.None(), - }; - let result = py_fn.call1(py, (name, py_req, py_ann)).map_err(|e| { - FlowError::Internal(format!("LLM request intercept callable failed: {e}")) - })?; + move |name: String, request: LlmRequest, annotated: Option| { + let py_fn = py_fn.clone(); + Box::pin(async move { + let result = resolve_py_object_or_future(Python::attach(|py| { + let py_req = PyLLMRequest { inner: request }; + let py_ann: Py = match annotated { + Some(ann) => { + let wrapper = PyAnnotatedLLMRequest { inner: ann }; + wrapper + .into_pyobject(py) + .map_err(|e| { + FlowError::Internal(format!( + "Failed to convert AnnotatedLLMRequest to Python: {e}" + )) + })? + .into_any() + .unbind() + } + None => py.None(), + }; + let result = py_fn.call1(py, (name, py_req, py_ann)).map_err(|e| { + FlowError::Internal(format!("LLM request intercept callable failed: {e}")) + })?; - result - .extract::(py) - .map(|value| value.inner) - .map_err(|e| { - FlowError::Internal(format!( - "LLM request intercept must return LLMRequestInterceptOutcome: {e}" - )) - }) + split_py_object_or_future(py, result) + })) + .await?; + Python::attach(|py| { + result + .extract::(py) + .map(|value| value.inner) + .map_err(|e| { + FlowError::Internal(format!( + "LLM request intercept must return LLMRequestInterceptOutcome: {e}" + )) + }) + }) }) }, ) @@ -1014,33 +1028,29 @@ pub fn wrap_py_finalizer_fn(py_fn: Py) -> Box Json + Send /// Wrap a Python callable `(Json, LlmSanitizeResponseContext) -> Optional[Json]`. fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeResponseFn { + let py_fn = Arc::new(py_fn); Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { - Python::attach(|py| { - let py_context = PyLlmSanitizeResponseContext { inner: context }; - let py_response = match json_to_py(py, &response) { - Ok(value) => value, - Err(error) => { - eprintln!("nemo_relay: json_to_py failed in LLM sanitize response: {error}"); - return None; - } - }; - let result = match py_fn.call1(py, (py_response, py_context)) { - Ok(value) => value, - Err(error) => { - eprintln!("nemo_relay: LLM sanitize response callable failed: {error}"); - return None; - } - }; - if result.is_none(py) { - return None; - } - match py_to_json(result.bind(py)) { - Ok(response) => Some(response), - Err(error) => { - eprintln!("nemo_relay: py_to_json failed in LLM sanitize response: {error}"); - None + let py_fn = py_fn.clone(); + Box::pin(async move { + let result = resolve_py_object_or_future(Python::attach(|py| { + let py_context = PyLlmSanitizeResponseContext { inner: context }; + let py_response = json_to_py(py, &response) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = py_fn + .call1(py, (py_response, py_context)) + .map_err(|error| FlowError::Internal(error.to_string()))?; + split_py_object_or_future(py, result) + })) + .await?; + Python::attach(|py| { + if result.is_none(py) { + Ok(None) + } else { + py_to_json(result.bind(py)) + .map(Some) + .map_err(|error| FlowError::Internal(error.to_string())) } - } + }) }) }) } @@ -1084,61 +1094,71 @@ pub fn wrap_py_event_subscriber(py_fn: Py) -> EventSubscriberFn { /// Wrap a Python callable ``(Event, EventSanitizeFields) -> EventSanitizeFields``. pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { - Arc::new(move |event: &Event, fields: EventSanitizeFields| { - Python::attach(|py| { - let py_event = match event { - Event::Scope(inner) => Py::new( - py, - crate::py_types::PyScopeEvent { - inner: inner.clone(), - }, - ) - .map(|value| value.into_any()), - Event::Mark(inner) => Py::new( - py, - crate::py_types::PyMarkEvent { - inner: inner.clone(), - }, - ) - .map(|value| value.into_any()), - }; - let py_event = match py_event { - Ok(value) => value, - Err(error) => { - eprintln!("nemo_relay: failed to convert event sanitizer context: {error}"); - return EventSanitizeFields::default(); - } - }; - let fields_json = match serde_json::to_value(&fields) { - Ok(value) => value, - Err(error) => { - eprintln!("nemo_relay: failed to serialize event sanitizer fields: {error}"); - return EventSanitizeFields::default(); - } - }; - let py_fields = match json_to_py(py, &fields_json) { - Ok(value) => value, - Err(error) => { - eprintln!("nemo_relay: failed to convert event sanitizer fields: {error}"); - return EventSanitizeFields::default(); - } - }; - let result = match py_fn.call1(py, (py_event, py_fields)) { - Ok(value) => value, - Err(error) => { - eprintln!("nemo_relay: Python event sanitizer callable failed: {error}"); - return EventSanitizeFields::default(); - } - }; - py_to_json(result.bind(py)) - .ok() - .and_then(|value| serde_json::from_value(value).ok()) - .unwrap_or_else(|| { - eprintln!( - "nemo_relay: event sanitizer must return data, category_profile, and metadata fields" - ); - EventSanitizeFields::default() - }) + let py_fn = Arc::new(py_fn); + Arc::new(move |event: Event, fields: EventSanitizeFields| { + let py_fn = py_fn.clone(); + Box::pin(async move { + let result = Python::attach( + |py| -> FlowResult, PyValueFuture>> { + let py_event = match &event { + Event::Scope(inner) => Py::new( + py, + crate::py_types::PyScopeEvent { + inner: inner.clone(), + }, + ) + .map(|value| value.into_any()), + Event::Mark(inner) => Py::new( + py, + crate::py_types::PyMarkEvent { + inner: inner.clone(), + }, + ) + .map(|value| value.into_any()), + }; + let py_event = match py_event { + Ok(value) => value, + Err(error) => { + eprintln!( + "nemo_relay: failed to convert event sanitizer context: {error}" + ); + return Err(FlowError::Internal(error.to_string())); + } + }; + let fields_json = match serde_json::to_value(&fields) { + Ok(value) => value, + Err(error) => { + eprintln!( + "nemo_relay: failed to serialize event sanitizer fields: {error}" + ); + return Err(FlowError::Internal(error.to_string())); + } + }; + let py_fields = match json_to_py(py, &fields_json) { + Ok(value) => value, + Err(error) => { + eprintln!( + "nemo_relay: failed to convert event sanitizer fields: {error}" + ); + return Err(FlowError::Internal(error.to_string())); + } + }; + let result = py_fn + .call1(py, (py_event, py_fields)) + .map_err(|error| FlowError::Internal(error.to_string()))?; + split_py_object_or_future(py, result) + }, + ); + let result = resolve_py_object_or_future(result).await?; + Python::attach(|py| { + py_to_json(result.bind(py)) + .map_err(|error| FlowError::Internal(error.to_string())) + .and_then(|value| { + serde_json::from_value(value).map_err(|error| { + FlowError::Internal(format!("invalid event sanitizer result: {error}")) + }) + }) + }) }) }) } diff --git a/crates/python/tests/coverage/coverage_tests.rs b/crates/python/tests/coverage/coverage_tests.rs index 67cebd3ba..2fa805abf 100644 --- a/crates/python/tests/coverage/coverage_tests.rs +++ b/crates/python/tests/coverage/coverage_tests.rs @@ -661,51 +661,68 @@ def event_fail(event): "#, ); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); let tool_ok = wrap_py_tool_fn(module.getattr("tool_ok").unwrap().unbind()); assert_eq!( - tool_ok("demo", json!({"x": 1})), + runtime + .block_on(tool_ok("demo".to_string(), json!({"x": 1}))) + .unwrap(), json!({"seen": 1, "name": "demo"}) ); let tool_fail = wrap_py_tool_fn(module.getattr("tool_fail").unwrap().unbind()); - assert_eq!(tool_fail("demo", json!({"x": 1})), json!({"x": 1})); + assert!( + runtime + .block_on(tool_fail("demo".to_string(), json!({"x": 1}))) + .is_err() + ); let tool_cond = wrap_py_tool_conditional_fn(module.getattr("tool_cond_bad").unwrap().unbind()); + let error = runtime + .block_on(tool_cond("demo".to_string(), json!({"x": 1}))) + .unwrap_err(); assert!( - tool_cond("demo", &json!({"x": 1})) - .unwrap_err() - .to_string() - .contains("expected str or None") + error.to_string().contains("unexpected type"), + "unexpected tool conditional error: {error}" ); let request = make_request(); let llm_sanitize = wrap_py_llm_sanitize_request_fn(module.getattr("llm_sanitize_bad").unwrap().unbind()) .unwrap(); - assert_eq!( - llm_sanitize( - request.clone(), - nemo_relay::api::runtime::LlmSanitizeRequestContext::default(), - ), - None + assert!( + runtime + .block_on(llm_sanitize( + request.clone(), + nemo_relay::api::runtime::LlmSanitizeRequestContext::default(), + )) + .is_err() ); let llm_cond = wrap_py_llm_conditional_fn(module.getattr("llm_cond_bad").unwrap().unbind()); assert!( - llm_cond(&request) + runtime + .block_on(llm_cond(request.clone())) .unwrap_err() .to_string() - .contains("expected str or None") + .contains("unexpected type") ); let llm_cond_none = wrap_py_llm_conditional_fn(module.getattr("llm_cond_none").unwrap().unbind()); - assert_eq!(llm_cond_none(&request).unwrap(), None); + assert_eq!( + runtime.block_on(llm_cond_none(request.clone())).unwrap(), + None + ); let llm_req = wrap_py_llm_request_intercept_fn(module.getattr("llm_req_bad").unwrap().unbind()); assert!( - llm_req("demo", request.clone(), None) + runtime + .block_on(llm_req("demo".to_string(), request.clone(), None)) .unwrap_err() .to_string() .contains("intercept callable failed") @@ -714,21 +731,21 @@ def event_fail(event): let tool_req = wrap_py_tool_request_intercept_fn(module.getattr("tool_fail").unwrap().unbind()); assert!( - tool_req("demo", json!({"x": 1})) - .unwrap_err() - .to_string() - .contains("Python tool callable failed") + runtime + .block_on(tool_req("demo".to_string(), json!({"x": 1}))) + .is_err() ); let llm_resp = wrap_py_llm_sanitize_response_fn(module.getattr("llm_resp_fail").unwrap().unbind()) .unwrap(); - assert_eq!( - llm_resp( - json!({"ok": true}), - nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), - ), - None + assert!( + runtime + .block_on(llm_resp( + json!({"ok": true}), + nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), + )) + .is_err() ); let mut collector = diff --git a/crates/python/tests/coverage/py_api_coverage_tests.rs b/crates/python/tests/coverage/py_api_coverage_tests.rs index 0bfe38573..c1cc50293 100644 --- a/crates/python/tests/coverage/py_api_coverage_tests.rs +++ b/crates/python/tests/coverage/py_api_coverage_tests.rs @@ -472,18 +472,31 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute ) .unwrap(); - let tool_intercepted = - tool_request_intercepts(py, "demo-tool", &py_dict(py, json!({"value": 1}))).unwrap(); + let tool_intercepted = tool_request_intercepts( + py, + "demo-tool".to_string(), + &py_dict(py, json!({"value": 1})), + ) + .unwrap(); assert_eq!( - crate::convert::py_to_json(tool_intercepted.bind(py)).unwrap(), + crate::convert::py_to_json(&tool_intercepted).unwrap(), json!({"value": 3}) ); - tool_conditional_execution("demo-tool", &py_dict(py, json!({"value": 1}))).unwrap(); + tool_conditional_execution( + py, + "demo-tool".to_string(), + &py_dict(py, json!({"value": 1})), + ) + .unwrap(); assert!( - tool_conditional_execution("demo-tool", &py_dict(py, json!({"value": -1}))) - .unwrap_err() - .to_string() - .contains("blocked") + tool_conditional_execution( + py, + "demo-tool".to_string(), + &py_dict(py, json!({"value": -1})) + ) + .unwrap_err() + .to_string() + .contains("blocked") ); let llm_request = PyLLMRequest { @@ -492,7 +505,10 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute content: json!({"messages": [{"role": "user", "content": "hello"}], "model": "demo-model"}), }, }; - let intercepted_request = llm_request_intercepts("demo-llm", llm_request.clone()).unwrap(); + let intercepted_request = + llm_request_intercepts(py, "demo-llm".to_string(), llm_request.clone()).unwrap(); + let intercepted_request: PyRef<'_, crate::py_types::PyLLMRequestInterceptOutcome> = + intercepted_request.extract().unwrap(); assert_eq!( intercepted_request .inner @@ -501,14 +517,17 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute .get("x-intercepted"), Some(&json!("1")) ); - llm_conditional_execution(llm_request.clone()).unwrap(); + llm_conditional_execution(py, llm_request.clone()).unwrap(); assert!( - llm_conditional_execution(PyLLMRequest { - inner: nemo_relay::api::llm::LlmRequest { - headers: serde_json::Map::new(), - content: json!({"messages": [], "model": "blocked"}), - }, - }) + llm_conditional_execution( + py, + PyLLMRequest { + inner: nemo_relay::api::llm::LlmRequest { + headers: serde_json::Map::new(), + content: json!({"messages": [], "model": "blocked"}), + }, + } + ) .unwrap_err() .to_string() .contains("blocked") diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index fe815cc1b..89a382f88 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -153,7 +153,13 @@ class RaisingResponseCodec: "model": "codec-model" })) .unwrap(); - let outcome = request_intercept("llm", make_request(), Some(annotated.clone())).unwrap(); + let outcome = runtime + .block_on(request_intercept( + "llm".to_string(), + make_request(), + Some(annotated.clone()), + )) + .unwrap(); assert_eq!( outcome.annotated_request.unwrap().last_user_message(), Some("annotated") @@ -163,7 +169,12 @@ class RaisingResponseCodec: module.getattr("request_bad_annotated").unwrap().unbind(), ); assert!( - bad_request_intercept("llm", make_request(), Some(annotated)) + runtime + .block_on(bad_request_intercept( + "llm".to_string(), + make_request(), + Some(annotated), + )) .unwrap_err() .to_string() .contains("must return LLMRequestInterceptOutcome") @@ -173,7 +184,12 @@ class RaisingResponseCodec: module.getattr("request_short_tuple").unwrap().unbind(), ); assert!( - short_request_intercept("llm", make_request(), None) + runtime + .block_on(short_request_intercept( + "llm".to_string(), + make_request(), + None + )) .unwrap_err() .to_string() .contains("must return LLMRequestInterceptOutcome") @@ -189,12 +205,13 @@ class RaisingResponseCodec: let llm_response = wrap_py_llm_sanitize_response_fn(module.getattr("llm_resp_bad_json").unwrap().unbind()) .unwrap(); - assert_eq!( - llm_response( - json!({"ok": true}), - nemo_relay::api::runtime::LlmSanitizeResponseContext::default() - ), - None + assert!( + runtime + .block_on(llm_response( + json!({"ok": true}), + nemo_relay::api::runtime::LlmSanitizeResponseContext::default() + )) + .is_err() ); let bad_codec = PyLlmCodecWrapper { @@ -683,23 +700,31 @@ def invalid(event, fields): metadata: Some(json!({"secret": true})), }; - let sanitized = wrap_py_event_sanitize_fn(module.getattr("sanitize").unwrap().unbind())( - &event, - fields.clone(), - ); + let runtime = tokio::runtime::Runtime::new().unwrap(); + let sanitized = runtime + .block_on(wrap_py_event_sanitize_fn( + module.getattr("sanitize").unwrap().unbind(), + )(event.clone(), fields.clone())) + .unwrap(); assert_eq!(sanitized.data, Some(json!({"safe": "checkpoint"}))); assert_eq!(sanitized.metadata, None); - let raised = wrap_py_event_sanitize_fn(module.getattr("raises").unwrap().unbind())( - &event, - fields.clone(), - ); - assert_eq!(raised, EventSanitizeFields::default()); - - let invalid = wrap_py_event_sanitize_fn(module.getattr("invalid").unwrap().unbind())( - &event, - fields.clone(), + let raised = runtime + .block_on(wrap_py_event_sanitize_fn( + module.getattr("raises").unwrap().unbind(), + )(event.clone(), fields.clone())) + .unwrap_err(); + assert!(raised.to_string().contains("sanitize boom")); + + let invalid = runtime + .block_on(wrap_py_event_sanitize_fn( + module.getattr("invalid").unwrap().unbind(), + )(event, fields.clone())) + .unwrap_err(); + assert!( + invalid + .to_string() + .contains("invalid event sanitizer result") ); - assert_eq!(invalid, EventSanitizeFields::default()); }); } diff --git a/docs/about-nemo-relay/concepts/middleware.mdx b/docs/about-nemo-relay/concepts/middleware.mdx index 0afcd2d56..afd1ae0cf 100644 --- a/docs/about-nemo-relay/concepts/middleware.mdx +++ b/docs/about-nemo-relay/concepts/middleware.mdx @@ -19,6 +19,19 @@ events. NeMo Relay applies each surface at a specific lifecycle point. Middleware is organized by lifecycle meaning rather than as one undifferentiated hook system. +## Asynchronous Callbacks + +All middleware families accept asynchronous callbacks. Rust callbacks return a +future; Python callbacks may return a value or an awaitable; and Node callbacks +may return a value or a Promise. Relay awaits entries sequentially in priority +order, so later callbacks observe earlier middleware output. + +Managed execution and standalone conditional/request-intercept helpers are +asynchronous because their result depends on middleware completion. Manual +lifecycle APIs (`tool_call`, `tool_call_end`, `llm_call`, and `llm_call_end`) +remain synchronous: they create or close their handle immediately and queue +observability work rather than awaiting it. + ## Registration Levels Middleware and subscribers can be registered at different levels depending on their @@ -125,6 +138,19 @@ context. For the callback contract and binding APIs, refer to Sanitize guardrails are observability-oriented. They do not rewrite the real arguments passed to the callback or the real value returned to the caller. +## Queued Event Publication + +Scope operations, marks, and manual tool/LLM lifecycle calls never become +awaitable because an event sanitizer is asynchronous. At emission time Relay +snapshots the event, visible sanitizer chain, and subscribers, then places the +work on a serial dispatcher. The dispatcher awaits sanitizers and publishes the +event later in FIFO order. + +Subscriber and exporter delivery is therefore delayed, while start/end/mark +order is preserved. Closing a scope or deregistering middleware after emission +does not affect queued snapshots. Sanitizer failures fail open: Relay records +the callback failure and publishes the last valid event snapshot. + ## Managed Execution Order For managed execution, NeMo Relay applies middleware and emits lifecycle events diff --git a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx index a4c162368..8bb88a328 100644 --- a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx +++ b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx @@ -1,6 +1,6 @@ --- title: "Native Dynamic Plugins (Rust)" -description: "Build in-process Rust shared-library plugins against the NeMo Relay Native ABI v2." +description: "Build in-process Rust shared-library plugins against the NeMo Relay Native ABI v3." position: 10 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. @@ -116,10 +116,12 @@ path, then replace `` with that library's SHA-256 digest. Use Native Plugin](/build-plugins/dynamic-plugins/native-dynamic/rust-native-plugin-example) for a complete example with validation, middleware, scopes, and configuration schema support. -## Native ABI v2 +## Native ABI v3 -The host passes a `NemoRelayNativeHostApiV1` table to the entry symbol. The -plugin returns a `NemoRelayNativePluginV1` descriptor: +The entry symbol receives a `*const NemoRelayNativeHostApiV1` pointer. It +points at the v1 prefix of a v3 `NemoRelayNativeHostApiV3` table; check +`abi_version` and `struct_size` before casting. The plugin returns a +`NemoRelayNativePluginV1` descriptor: ```rust extern "C" fn nemo_relay_register_plugin( @@ -128,6 +130,35 @@ extern "C" fn nemo_relay_register_plugin( ) -> NemoRelayStatus ``` +The v3 host table retains the frozen legacy prefix and appends a +completion-based asynchronous middleware extension. An entry that rejects the +v3 table with `InvalidArg` is retried with the legacy table. Rust plugins using the +typed `NativePlugin` APIs continue to work unchanged. Raw ABI plugins can use +`PluginContext::register_async_middleware_raw` when a callback must complete +later. The callback receives a JSON invocation, an optional continuation for +execution intercepts, and a one-shot completion handle. + +Return `Complete` after resolving or rejecting the completion before the +callback returns. Return `Pending` only when retaining the completion; settle +it exactly once, call `async_completion_release`, and release an async `next` +handle after use. The host marks a completion cancelled when the awaiting +runtime work is dropped; late and duplicate settlement is rejected safely. + +Event sanitizers registered through this extension still run on Relay's serial +publication dispatcher. Scope and mark emission remain synchronous and their +sanitized events are delivered later in emission order. + +The v3 completion and continuation ABI settles one JSON value. Consequently, +an async native LLM stream execution intercept currently receives and returns +the complete JSON array of chunks: Relay buffers the provider stream before +replaying it to the caller. It is not an incremental streaming transport and +does not provide per-chunk backpressure. Use a synchronous native stream +intercept or a worker plugin when first-token latency is required. + +Legacy v1/v2 middleware callbacks are synchronous and run on the runtime's +execution path. They must not block on I/O; use the v3 completion-based API for +long-running work. + Text and JSON data cross this boundary as host-owned `NemoRelayNativeString` handles. ABI structs also carry scalars, opaque handles, callback pointers, and plugin-owned `user_data`. Do not pass Rust diff --git a/docs/reference/event-sanitizers.mdx b/docs/reference/event-sanitizers.mdx index 8a4689662..6ff89e7e7 100644 --- a/docs/reference/event-sanitizers.mdx +++ b/docs/reference/event-sanitizers.mdx @@ -49,22 +49,25 @@ semantic category, attributes, semantic input and output meaning, or schemas. Registries run in priority order. Lower priorities run first, and each callback receives the fields returned by the callback before it. Invalid -binding callback results fail open and preserve the current fields. In Node.js, +binding callback results fail open and preserve the current fields. The same +rule applies to tool and LLM request/response sanitizer errors: Relay preserves +the last valid observability payload without changing provider execution. In Node.js, a synchronous sanitizer callback that throws also fails open; Relay records the error for `getLastCallbackError()`. -## Publication Semantics +## Async Delivery and Ordering -Scope and mark emission APIs remain synchronous. They snapshot the event, -visible sanitizer chain, and subscribers, then enqueue that snapshot for -sanitization and publication on a serial background dispatcher. Subscribers -and exporters therefore receive the sanitized event after the emission call -returns. +Event sanitizer callbacks may be asynchronous: use an `async def` callback in +Python, return a Promise in Node.js, or return a future in Rust. Scope and mark +emission remains synchronous. Relay snapshots the event, sanitizers, and +subscribers and queues them on one serial publication dispatcher; that +dispatcher awaits sanitizers before delivering the event to subscribers and +exporters. -The dispatcher processes snapshots in FIFO order, preserving scope start/end -and mark ordering. Closing a scope or deregistering middleware after emission -does not alter an already-snapshotted publication chain. Use the binding's -subscriber flush API when a test or shutdown path must wait for queued delivery. +This preserves FIFO start/end/mark delivery without making `push_scope`, +`pop_scope`, or `event` awaitable. A scope-local sanitizer removed after an +event is emitted still applies to its queued snapshot. An asynchronous +sanitizer rejection fails open and preserves the last valid event fields. ## Registration Lifetimes @@ -187,17 +190,23 @@ activation fails. ## Experimental C and Go Bindings -The source-first C API uses `NemoRelayEventSanitizeCb`. It provides global, -scope-local, and plugin-context registration functions for all three surfaces. -Global names start with `nemo_relay_register_`, and scope-local names start -with `nemo_relay_scope_register_`. - -The Go binding provides `EventSanitizeFields`, `EventSanitizeFunc`, global -`Register*SanitizeGuardrail` helpers, scope-local -`ScopeRegister*SanitizeGuardrail` helpers, and the same methods on -`PluginContext`. The `guardrails` package provides shorter aliases. Because a -returned `EventSanitizeFields` replaces all three fields, copy the supplied -value and modify only the fields that should change. +The source-first C API retains `NemoRelayEventSanitizeCb` and adds parallel +completion-based async registration APIs. An async callback returns `Complete` +or `Pending` and settles its one-shot completion handle with resolve or reject; +there is no implicit timeout. A callback that returns `Pending` must settle the +handle exactly once, or serial event publication remains blocked. Relay cancels +the handle when the invocation is abandoned; late or duplicate settlement after +cancellation is rejected safely. After resolving or rejecting a retained +completion, call `nemo_relay_async_completion_release` to release the +callback-owned reference. Global names start with `nemo_relay_register_`, and +scope-local names start with `nemo_relay_scope_register_`. + +The Go binding provides `EventSanitizeFields`, `EventSanitizeFunc`, and +`AsyncMiddlewareFunc` variants for global and scope-local event sanitizers. +Async Go callbacks receive a `context.Context`; Relay cancels it when the +invocation is abandoned. Because a returned `EventSanitizeFields` replaces all +three fields, copy the supplied value and modify only the fields that should +change. ## Related Topics diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index 5f5b6aca3..dac53db81 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -1,6 +1,6 @@ --- title: "Migration Guides" -description: "Upgrade NeMo Relay integrations and migrate LLM sanitizer callbacks, plugins, workers, and PII policy." +description: "Upgrade NeMo Relay integrations and migrate async middleware, plugins, workers, LLM sanitizers, and PII policy." position: 6 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. @@ -13,17 +13,64 @@ intervening release in sequence. ## Upgrade to NeMo Relay 0.7 -NeMo Relay 0.7 changes the LLM observability sanitizer contract across -in-process bindings, native plugins, raw C FFI consumers, and worker plugins. -Complete the following migrations before you run an existing sanitizer with a -0.7 host. +NeMo Relay 0.7 makes the Rust middleware callback contract asynchronous and +adds awaitable middleware support across the in-process bindings, native +plugins, raw C FFI consumers, and worker plugins. It also changes the LLM +observability sanitizer contract. Complete the following migrations before you +run existing middleware or a sanitizer with a 0.7 host. -Do not deploy a 0.6 sanitizer plugin or worker against a 0.7 host. The LLM -callback signature, native ABI layout, and worker invocation schema changed. -NeMo Relay does not adapt one-argument LLM sanitizer callbacks. +Do not deploy a 0.6 plugin or worker against a 0.7 host. The middleware +callback contract, LLM callback signature, native ABI layout, and worker +invocation schema changed. NeMo Relay does not adapt synchronous Rust +middleware callbacks or one-argument LLM sanitizer callbacks. +### Migrate Middleware Callbacks + +The following callback families are now asynchronous: conditional execution +guardrails, request intercepts, execution intercepts, tool and LLM sanitizers, +and event sanitizers. Relay awaits each registered callback sequentially in +priority order. A callback that rejects or returns an error preserves the +existing error behavior for its middleware family. + +| Surface | 0.6 Callback | 0.7 Callback | +| --- | --- | --- | +| Rust | `Fn(...) -> Result` | `Fn(...) -> Pin> + Send>>` | +| Python | Direct return value | Direct return value or awaitable | +| Node.js | Direct return value | Direct return value or `Promise` | +| Go / raw C FFI | Synchronous callback | Existing synchronous callback, or the new `Async` / completion-based registration API | + +For Rust, wrap the existing result in a ready async future, or use an async +block when the callback needs to await work: + +```rust +use std::sync::Arc; + +use nemo_relay::api::registry::register_tool_conditional_execution_guardrail; + +register_tool_conditional_execution_guardrail( + "policy", + 10, + Arc::new(|_name, _args| { + Box::pin(async move { + // Await policy I/O here when needed. + Ok(None) // Return Some(reason) to block execution. + }) + }), +)?; +``` + +Python and Node.js registration names are unchanged. Mark a Python callback +`async def`, or return a Promise from Node.js, only when it needs asynchronous +work; existing direct-value callbacks remain supported. + +Scope lifecycle and mark emission remain synchronous. `push_scope`, +`pop_scope`, and mark APIs snapshot the event and visible sanitizer/subscriber +chain, then enqueue sanitization and publication on a serial dispatcher. Event +subscribers and exporters therefore receive sanitized events later, in emission +order. Do not add `await` to scope or mark emission calls. + ### Update LLM Sanitizer Callbacks The registration names remain unchanged for global, plugin-context, and @@ -49,9 +96,11 @@ register_llm_sanitize_request_guardrail( "redact-request", 10, Arc::new(|request, context| { - let _active_codec = context.resolve_codec(); - // Apply policy, using _active_codec when normalized access is required. - Some(request) + Box::pin(async move { + let _active_codec = context.resolve_codec(); + // Apply policy, using _active_codec when normalized access is required. + Ok(Some(request)) + }) }), )?; ``` @@ -150,15 +199,15 @@ For complete in-process examples, refer to ### Migrate Worker Sanitizers -All Rust worker sanitizer registrations now require callbacks that return -futures. This change applies to mark, scope-start, scope-end, tool-request, -tool-response, LLM-request, and LLM-response sanitizers. Conditional guardrails -and request intercepts keep their existing synchronous contracts. +All Rust worker middleware registrations now require callbacks that return +futures. This includes conditional guardrails, request and execution intercepts, +mark and scope event sanitizers, tool request/response sanitizers, and LLM +request/response sanitizers. Python worker middleware can return either an +immediate value or an awaitable. Python LLM sanitizers must still accept both +the payload and directional context. -Update Rust worker callbacks to use `async move` and return `Result` from the -future. Python worker sanitizers can return either an immediate value or an -awaitable, but Python LLM sanitizers must still accept both the payload and -directional context. +Update Rust worker callbacks to return `Box::pin(async move { ... })` and +resolve to `Result` from the future. @@ -167,13 +216,15 @@ directional context. ctx.register_llm_sanitize_request_guardrail( "redact-request", 10, - |request, context| async move { - if let Some(codec) = context.resolve_codec() { - let annotated = codec.decode(&request).await?; - let request = codec.encode(&annotated, &request).await?; - return Ok(Some(request)); - } - Ok(Some(request)) + |request, context| { + Box::pin(async move { + if let Some(codec) = context.resolve_codec() { + let annotated = codec.decode(&request).await?; + let request = codec.encode(&annotated, &request).await?; + return Ok(Some(request)); + } + Ok(Some(request)) + }) }, ); ``` @@ -220,13 +271,14 @@ wrong-direction capability IDs. ### Rebuild Native and Raw FFI Plugins -NeMo Relay 0.7 uses native ABI v2. Recompile native plugins against the 0.7 +NeMo Relay 0.7 uses native ABI v3. Recompile native plugins against the 0.7 `nemo-relay-plugin` crate and rebuild raw FFI consumers against the generated 0.7 header. -If you already built a plugin against an earlier 0.7 ABI v2 prerelease, rebuild -it again. The ABI version remains 2, but the prerelease LLM sanitizer callback -slots and context layouts changed before release. +The v3 table preserves the v2 prefix, and Relay retries a legacy v2 table when +loading a plugin that rejects v3. Rebuild anyway if a plugin uses raw ABI +callbacks: v3 adds completion-based async middleware registration, async +execution continuations, and explicit cancellation/late-settlement behavior. The plugin manifest value remains `compat.native_api = "1"`. This manifest contract version is separate from the host ABI version; do not change it to @@ -243,7 +295,7 @@ after the callback returns. Release host-owned output strings with the standard host string release operation. For the complete ABI contract, refer to -[Native ABI v2](/build-plugins/dynamic-plugins/native-dynamic/about#native-abi-v2). +[Native ABI v3](/build-plugins/dynamic-plugins/native-dynamic/about#native-abi-v3). ### Update PII Redaction Configuration diff --git a/integrations/openclaw/test/live-smoke.test.ts b/integrations/openclaw/test/live-smoke.test.ts index c8b3de692..a7a788509 100644 --- a/integrations/openclaw/test/live-smoke.test.ts +++ b/integrations/openclaw/test/live-smoke.test.ts @@ -17,6 +17,17 @@ import { callGatewayStatus, type TestGatewayMethodHandler } from './gateway-stat const liveSmokeEnabled = process.env.NEMO_RELAY_OPENCLAW_LIVE_SMOKE === '1'; +async function waitForExportFile(outputDir: string, prefix: string, timeoutMs = 2_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const files = await fs.readdir(outputDir); + const exportedPath = files.find((file) => file.startsWith(prefix) && file.endsWith('.json')); + if (exportedPath) return exportedPath; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + return undefined; +} + it( 'runs a live NeMo Relay binding smoke for session ATIF export and hook replay', { skip: !liveSmokeEnabled }, @@ -133,8 +144,7 @@ it( { sessionId: '../live-session:1' }, ); - const files = await fs.readdir(outputDir); - const exportedPath = files.find((file) => file.startsWith('live-') && file.endsWith('.json')); + const exportedPath = await waitForExportFile(outputDir, 'live-'); assert.ok(exportedPath, 'expected generic observability ATIF export'); const exported = JSON.parse(await fs.readFile(path.join(outputDir, exportedPath), 'utf8')) as unknown; assert.equal(typeof exported, 'object'); diff --git a/python/nemo_relay/__init__.py b/python/nemo_relay/__init__.py index 9a7690b96..1b6a3a9ee 100644 --- a/python/nemo_relay/__init__.py +++ b/python/nemo_relay/__init__.py @@ -167,26 +167,33 @@ class EventSanitizeFields(TypedDict): #: Arguments are the tool name and JSON payload. The return value is the JSON #: payload recorded on the emitted event. Exceptions propagate through the #: lifecycle call that invoked the guardrail. -ToolSanitizeGuardrail: TypeAlias = Callable[[str, Json], Json] -EventSanitizeGuardrail: TypeAlias = Callable[["Event", EventSanitizeFields], EventSanitizeFields] +ToolSanitizeGuardrail: TypeAlias = Callable[[str, Json], Json | Awaitable[Json]] +EventSanitizeGuardrail: TypeAlias = Callable[ + ["Event", EventSanitizeFields], EventSanitizeFields | Awaitable[EventSanitizeFields] +] #: Guardrail callback that can block tool execution by returning a rejection #: message. Returning ``None`` allows execution to continue. -ToolConditionalExecutionGuardrail: TypeAlias = Callable[[str, Json], Optional[str]] +ToolConditionalExecutionGuardrail: TypeAlias = Callable[[str, Json], Optional[str] | Awaitable[Optional[str]]] #: Guardrail callback that sanitizes an ``LLMRequest`` used for emitted events. #: Callbacks receive ``(request, context)``. Returning ``None`` omits the LLM observability #: payload and annotation without changing the caller-visible request. -LlmSanitizeRequestGuardrail: TypeAlias = Callable[[LLMRequest, "LlmSanitizeRequestContext"], Optional[LLMRequest]] +LlmSanitizeRequestGuardrail: TypeAlias = Callable[ + [LLMRequest, "LlmSanitizeRequestContext"], + Optional[LLMRequest] | Awaitable[Optional[LLMRequest]], +] #: Guardrail callback that sanitizes an emitted JSON LLM response payload. #: Callbacks receive ``(response, context)`` and can return ``None`` to omit #: observability payload and annotation without changing the caller response. -LlmSanitizeResponseGuardrail: TypeAlias = Callable[[Json, "LlmSanitizeResponseContext"], Optional[Json]] +LlmSanitizeResponseGuardrail: TypeAlias = Callable[ + [Json, "LlmSanitizeResponseContext"], Optional[Json] | Awaitable[Optional[Json]] +] #: Guardrail callback that can block an LLM call by returning a rejection #: message. Returning ``None`` allows execution to continue. -LlmConditionalExecutionGuardrail: TypeAlias = Callable[[LLMRequest], Optional[str]] +LlmConditionalExecutionGuardrail: TypeAlias = Callable[[LLMRequest], Optional[str] | Awaitable[Optional[str]]] #: Request intercept callback that rewrites tool arguments before execution. #: Arguments are the tool name and current JSON payload. The return value #: becomes the payload seen by later request intercepts and tool execution. -ToolRequestIntercept: TypeAlias = AbcCallable[[str, Json], Json] +ToolRequestIntercept: TypeAlias = AbcCallable[[str, Json], Json | Awaitable[Json]] #: Execution intercept callback that wraps tool execution with middleware #: behavior. The callback receives the tool name, current arguments, and the #: next callable. It may await and return ``next(args)`` or short-circuit. @@ -198,7 +205,7 @@ class EventSanitizeFields(TypedDict): #: and pending-mark outcome passed to later intercepts and managed execution. LlmRequestIntercept: TypeAlias = Callable[ [str, LLMRequest, AnnotatedLLMRequest | None], - LLMRequestInterceptOutcome, + LLMRequestInterceptOutcome | Awaitable[LLMRequestInterceptOutcome], ] #: Execution intercept callback that wraps non-streaming LLM execution. The #: callback receives the logical LLM name, request, and next callable. It may diff --git a/python/nemo_relay/__init__.pyi b/python/nemo_relay/__init__.pyi index 2e8f5b0fc..f640a2f72 100644 --- a/python/nemo_relay/__init__.pyi +++ b/python/nemo_relay/__init__.pyi @@ -161,8 +161,11 @@ class EventSanitizeFields(TypedDict): category_profile: JsonObject | None metadata: Json | None -ToolSanitizeGuardrail: TypeAlias = Callable[[str, Json], Json] -EventSanitizeGuardrail: TypeAlias = Callable[[Event, EventSanitizeFields], EventSanitizeFields] +ToolSanitizeGuardrail: TypeAlias = Callable[[str, Json], Json | Awaitable[Json]] +EventSanitizeGuardrail: TypeAlias = Callable[ + [Event, EventSanitizeFields], + EventSanitizeFields | Awaitable[EventSanitizeFields], +] """Guardrail callback that sanitizes emitted tool request or response payloads. Arguments: @@ -175,7 +178,7 @@ Exceptional flow: Exceptions raised by the callback propagate through the lifecycle operation that invoked the guardrail. """ -ToolConditionalExecutionGuardrail: TypeAlias = Callable[[str, Json], Optional[str]] +ToolConditionalExecutionGuardrail: TypeAlias = Callable[[str, Json], Optional[str] | Awaitable[Optional[str]]] """Guardrail callback that can block tool execution. Arguments: @@ -184,7 +187,10 @@ Arguments: Return: ``None`` to allow execution, or a rejection message to block it. """ -LlmSanitizeRequestGuardrail: TypeAlias = Callable[[LLMRequest, "LlmSanitizeRequestContext"], Optional[LLMRequest]] +LlmSanitizeRequestGuardrail: TypeAlias = Callable[ + [LLMRequest, "LlmSanitizeRequestContext"], + Optional[LLMRequest] | Awaitable[Optional[LLMRequest]], +] """Guardrail callback that sanitizes an ``LLMRequest`` used for emitted events. Arguments: @@ -197,7 +203,10 @@ Return: Request object recorded on the emitted lifecycle event, or ``None`` to omit the LLM observability payload and annotation. """ -LlmSanitizeResponseGuardrail: TypeAlias = Callable[[Json, "LlmSanitizeResponseContext"], Optional[Json]] +LlmSanitizeResponseGuardrail: TypeAlias = Callable[ + [Json, "LlmSanitizeResponseContext"], + Optional[Json] | Awaitable[Optional[Json]], +] """Guardrail callback that sanitizes an emitted JSON LLM response payload. Arguments: @@ -210,7 +219,7 @@ Return: Response object recorded on the emitted lifecycle event, or ``None`` to omit the LLM observability payload and annotation. """ -LlmConditionalExecutionGuardrail: TypeAlias = Callable[[LLMRequest], Optional[str]] +LlmConditionalExecutionGuardrail: TypeAlias = Callable[[LLMRequest], Optional[str] | Awaitable[Optional[str]]] """Guardrail callback that can block an LLM call. Arguments: @@ -219,7 +228,7 @@ Arguments: Return: ``None`` to allow execution, or a rejection message to block it. """ -ToolRequestIntercept: TypeAlias = Callable[[str, Json], Json] +ToolRequestIntercept: TypeAlias = Callable[[str, Json], Json | Awaitable[Json]] """Request intercept callback that rewrites tool arguments before execution. Arguments: @@ -246,7 +255,7 @@ Exceptional flow: """ LlmRequestIntercept: TypeAlias = Callable[ [str, LLMRequest, AnnotatedLLMRequest | None], - LLMRequestInterceptOutcome, + LLMRequestInterceptOutcome | Awaitable[LLMRequestInterceptOutcome], ] """Request intercept callback that rewrites raw and annotated LLM requests. diff --git a/python/nemo_relay/_native.pyi b/python/nemo_relay/_native.pyi index 1b4e7354a..d4f409405 100644 --- a/python/nemo_relay/_native.pyi +++ b/python/nemo_relay/_native.pyi @@ -37,20 +37,29 @@ class _EventSanitizeFields(TypedDict): category_profile: _JsonObject | None metadata: _Json | None -_ToolSanitizeGuardrail: TypeAlias = Callable[[str, _Json], _Json] -_ToolConditionalExecutionGuardrail: TypeAlias = Callable[[str, _Json], Optional[str]] -_LlmSanitizeRequestGuardrail: TypeAlias = Callable[["LLMRequest", "LlmSanitizeRequestContext"], Optional["LLMRequest"]] -_LlmSanitizeResponseGuardrail: TypeAlias = Callable[[_Json, "LlmSanitizeResponseContext"], Optional[_Json]] -_EventSanitizeGuardrail: TypeAlias = Callable[[ScopeEvent | MarkEvent, _EventSanitizeFields], _EventSanitizeFields] -_LlmConditionalExecutionGuardrail: TypeAlias = Callable[["LLMRequest"], Optional[str]] -_ToolRequestIntercept: TypeAlias = Callable[[str, _Json], _Json] +_ToolSanitizeGuardrail: TypeAlias = Callable[[str, _Json], _Json | Awaitable[_Json]] +_ToolConditionalExecutionGuardrail: TypeAlias = Callable[[str, _Json], Optional[str] | Awaitable[Optional[str]]] +_LlmSanitizeRequestGuardrail: TypeAlias = Callable[ + ["LLMRequest", "LlmSanitizeRequestContext"], + Optional["LLMRequest"] | Awaitable[Optional["LLMRequest"]], +] +_LlmSanitizeResponseGuardrail: TypeAlias = Callable[ + [_Json, "LlmSanitizeResponseContext"], + Optional[_Json] | Awaitable[Optional[_Json]], +] +_EventSanitizeGuardrail: TypeAlias = Callable[ + [ScopeEvent | MarkEvent, _EventSanitizeFields], + _EventSanitizeFields | Awaitable[_EventSanitizeFields], +] +_LlmConditionalExecutionGuardrail: TypeAlias = Callable[["LLMRequest"], Optional[str] | Awaitable[Optional[str]]] +_ToolRequestIntercept: TypeAlias = Callable[[str, _Json], _Json | Awaitable[_Json]] _ToolExecutionIntercept: TypeAlias = Callable[ [str, _Json, Callable[[_Json], Awaitable[_Json]]], "ToolExecutionInterceptOutcome | Awaitable[ToolExecutionInterceptOutcome]", ] _LlmRequestIntercept: TypeAlias = Callable[ [str, "LLMRequest", "AnnotatedLLMRequest | None"], - "LLMRequestInterceptOutcome", + "LLMRequestInterceptOutcome | Awaitable[LLMRequestInterceptOutcome]", ] _LlmExecutionIntercept: TypeAlias = Callable[ [str, "LLMRequest", Callable[["LLMRequest"], Awaitable[_Json]]], @@ -1615,7 +1624,7 @@ def llm_stream_call_execute( """ ... -def tool_request_intercepts(name: str, args: _Json) -> _Json: +def tool_request_intercepts(name: str, args: _Json) -> _Json | Awaitable[_Json]: """Run the registered tool request-intercept chain. Args: @@ -1623,14 +1632,15 @@ def tool_request_intercepts(name: str, args: _Json) -> _Json: args: Current JSON-compatible tool arguments. Returns: - Transformed tool arguments after all applicable request intercepts. + Transformed tool arguments directly outside an event loop, or an + awaitable resolving to them from an async caller. Exceptional flow: Callback exceptions and native middleware errors propagate unchanged. """ ... -def tool_conditional_execution(name: str, args: _Json) -> None: +def tool_conditional_execution(name: str, args: _Json) -> None | Awaitable[None]: """Run tool conditional-execution guardrails. Args: @@ -1638,7 +1648,8 @@ def tool_conditional_execution(name: str, args: _Json) -> None: args: Current JSON-compatible tool arguments. Returns: - ``None`` when all guardrails allow execution. + ``None`` when all guardrails allow execution, directly outside an event + loop or through an awaitable from an async caller. Exceptional flow: Raises a native rejection error when a guardrail returns a rejection @@ -1646,7 +1657,9 @@ def tool_conditional_execution(name: str, args: _Json) -> None: """ ... -def llm_request_intercepts(name: str, request: LLMRequest) -> LLMRequestInterceptOutcome: +def llm_request_intercepts( + name: str, request: LLMRequest +) -> LLMRequestInterceptOutcome | Awaitable[LLMRequestInterceptOutcome]: """Run the registered LLM request-intercept chain. Args: @@ -1654,21 +1667,23 @@ def llm_request_intercepts(name: str, request: LLMRequest) -> LLMRequestIntercep request: Current LLM request. Returns: - Transformed request after all applicable request intercepts. + Transformed request directly outside an event loop, or an awaitable + resolving to it from an async caller. Exceptional flow: Callback exceptions and native middleware errors propagate unchanged. """ ... -def llm_conditional_execution(request: LLMRequest) -> None: +def llm_conditional_execution(request: LLMRequest) -> None | Awaitable[None]: """Run LLM conditional-execution guardrails. Args: request: LLM request to evaluate. Returns: - ``None`` when all guardrails allow execution. + ``None`` when all guardrails allow execution, directly outside an event + loop or through an awaitable from an async caller. Exceptional flow: Raises a native rejection error when a guardrail returns a rejection diff --git a/python/tests/test_adaptive.py b/python/tests/test_adaptive.py index c7e956e7b..899c3b2e5 100644 --- a/python/tests/test_adaptive.py +++ b/python/tests/test_adaptive.py @@ -222,7 +222,7 @@ async def test_adaptive_runtime_bind_scope_passes_through_without_state(self): ) with scope.scope("adaptive-runtime-translate", ScopeType.Agent) as handle: runtime.bind_scope(handle) - translated = llm.request_intercepts("anthropic", request) + translated = await llm.request_intercepts("anthropic", request) assert translated.request.content == { "messages": [{"role": "user", "content": "Hello"}], "system": "You are helpful.", diff --git a/python/tests/test_builtin_codecs.py b/python/tests/test_builtin_codecs.py index 156122583..c1fcef898 100644 --- a/python/tests/test_builtin_codecs.py +++ b/python/tests/test_builtin_codecs.py @@ -12,8 +12,6 @@ from typing import cast -import pytest - import nemo_relay from nemo_relay import ( AnnotatedLLMRequest, @@ -434,8 +432,8 @@ def sanitize_response(response, context): subscribers.deregister("test-manual-call-end-sanitized-response-codec") guardrails.deregister_llm_sanitize_response("test-call-end-codec-sanitizer") - def test_manual_call_end_response_codec_failure_raises_after_end_event(self): - """manual llm.call_end() surfaces response codec failures instead of dropping them.""" + def test_manual_call_end_response_codec_failure_defers_without_raising(self): + """manual llm.call_end() records deferred response codec failures without blocking.""" captured_events = [] def capture(event): @@ -448,8 +446,7 @@ def capture(event): "manual-codec-error-llm", LLMRequest({}, {"model": "gpt-4", "messages": []}), ) - with pytest.raises(RuntimeError, match="OpenAI Chat response decode"): - llm.call_end(handle, "malformed response", response_codec=OpenAIChatCodec()) + llm.call_end(handle, "malformed response", response_codec=OpenAIChatCodec()) subscribers.flush() end_events = [ diff --git a/python/tests/test_context_isolation.py b/python/tests/test_context_isolation.py index 49314e312..7ee5a7531 100644 --- a/python/tests/test_context_isolation.py +++ b/python/tests/test_context_isolation.py @@ -199,8 +199,8 @@ async def run_tool(owner): ) await asyncio.sleep(0) - args = nemo_relay.tools.request_intercepts("task-tool", {"owner": owner}) - nemo_relay.tools.conditional_execution("task-tool", args) + args = await nemo_relay.tools.request_intercepts("task-tool", {"owner": owner}) + await nemo_relay.tools.conditional_execution("task-tool", args) manual_handle = nemo_relay.tools.call(f"manual-tool-{owner}", args) await asyncio.sleep(0) @@ -258,9 +258,9 @@ def intercept(name, request, annotated): request = nemo_relay.LLMRequest({}, {"messages": [], "owner": owner}) await asyncio.sleep(0) - intercepted = nemo_relay.llm.request_intercepts("task-llm", request) + intercepted = await nemo_relay.llm.request_intercepts("task-llm", request) assert intercepted.request.content["intercepted_by"] == owner - nemo_relay.llm.conditional_execution(request) + await nemo_relay.llm.conditional_execution(request) manual_handle = nemo_relay.llm.call(f"manual-llm-{owner}", request) await asyncio.sleep(0) diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 89d97d9d6..08234b70e 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -56,7 +56,7 @@ def second(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitiz assert calls == [("checkpoint", {"secret": "raw"}), ("mark", {"stage": "first"})] -def test_mark_sanitizer_exception_clears_observability_fields(capture_events): +def test_mark_sanitizer_exception_preserves_observability_fields(capture_events): _capture_name, events = capture_events def raises(_event: nemo_relay.Event, _fields: EventSanitizeFields) -> EventSanitizeFields: @@ -69,7 +69,7 @@ def raises(_event: nemo_relay.Event, _fields: EventSanitizeFields) -> EventSanit finally: guardrails.deregister_mark_sanitize("python-mark-raises") - assert events[-1].data is None + assert events[-1].data == {"kept": True} assert events[-1].metadata is None diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index 9a3662be4..411629401 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -167,6 +167,7 @@ def sanitize_response(response, context): try: handle = llm.call("py_llm_structured_context", make_request()) llm.call_end(handle, {"response": "ok"}) + subscribers.flush() finally: guardrails.deregister_llm_sanitize_request("py_llm_structured_context_request") guardrails.deregister_llm_sanitize_response("py_llm_structured_context_response") @@ -275,7 +276,7 @@ def test_duplicate_raises(self): guardrails.register_llm_sanitize_request("py_llm_dup", 1, lambda r, context: r) guardrails.deregister_llm_sanitize_request("py_llm_dup") - def test_sanitize_request_callable_error_omits_observability_input(self): + def test_sanitize_request_callable_error_preserves_observability_input(self): events = [] subscribers.register("py_llm_sanitize_req_sub", lambda event: events.append(event)) guardrails.register_llm_sanitize_request( @@ -284,7 +285,10 @@ def test_sanitize_request_callable_error_omits_observability_input(self): lambda request, context: raise_runtime_error("boom"), ) try: - request = make_request() + request = LLMRequest( + {"authorization": "secret", "x-request-id": "safe"}, + make_request().content, + ) handle = llm.call("llm_sanitize_req_fail", request) llm.call_end(handle, {"ok": True}) finally: @@ -295,10 +299,10 @@ def test_sanitize_request_callable_error_omits_observability_input(self): subscribers.deregister("py_llm_sanitize_req_sub") start = _llm_event(events, "llm_sanitize_req_fail", "start") - assert start.data is None + assert start.data == {"headers": {"x-request-id": "safe"}, "content": request.content} assert start.annotated_request is None - def test_sanitize_request_invalid_return_omits_observability_input(self): + def test_sanitize_request_invalid_return_preserves_observability_input(self): events = [] subscribers.register("py_llm_sanitize_req_bad_sub", lambda event: events.append(event)) guardrails.register_llm_sanitize_request( @@ -307,7 +311,10 @@ def test_sanitize_request_invalid_return_omits_observability_input(self): cast(guardrails.LlmSanitizeRequestGuardrail, lambda request, context: object()), ) try: - request = make_request() + request = LLMRequest( + {"authorization": "secret", "x-request-id": "safe"}, + make_request().content, + ) handle = llm.call("llm_sanitize_req_bad", request) llm.call_end(handle, {"ok": True}) finally: @@ -318,10 +325,10 @@ def test_sanitize_request_invalid_return_omits_observability_input(self): subscribers.deregister("py_llm_sanitize_req_bad_sub") start = _llm_event(events, "llm_sanitize_req_bad", "start") - assert start.data is None + assert start.data == {"headers": {"x-request-id": "safe"}, "content": request.content} assert start.annotated_request is None - def test_sanitize_response_callable_error_omits_observability_output(self): + def test_sanitize_response_callable_error_preserves_observability_output(self): events = [] subscribers.register("py_llm_sanitize_resp_sub", lambda event: events.append(event)) guardrails.register_llm_sanitize_response( @@ -340,10 +347,10 @@ def test_sanitize_response_callable_error_omits_observability_output(self): subscribers.deregister("py_llm_sanitize_resp_sub") end = _llm_event(events, "llm_sanitize_resp_fail", "end") - assert end.data is None + assert end.data == {"ok": True} assert end.annotated_response is None - def test_sanitize_response_invalid_return_omits_observability_output(self): + def test_sanitize_response_invalid_return_preserves_observability_output(self): events = [] subscribers.register("py_llm_sanitize_resp_bad_sub", lambda event: events.append(event)) guardrails.register_llm_sanitize_response( @@ -362,7 +369,7 @@ def test_sanitize_response_invalid_return_omits_observability_output(self): subscribers.deregister("py_llm_sanitize_resp_bad_sub") end = _llm_event(events, "llm_sanitize_resp_bad", "end") - assert end.data is None + assert end.data == {"ok": True} assert end.annotated_response is None def test_sanitize_response_guardrail_accepts_scalar_json_payloads(self): @@ -398,7 +405,7 @@ def test_conditional_execution_invalid_return_type_raises(self): cast(guardrails.LlmConditionalExecutionGuardrail, lambda request: 123), ) try: - with pytest.raises(RuntimeError, match="expected str or None"): + with pytest.raises(RuntimeError, match="unexpected type"): llm.conditional_execution(make_request()) finally: guardrails.deregister_llm_conditional_execution("py_llm_cond_bad_type") @@ -410,7 +417,7 @@ def test_conditional_execution_callable_error_raises(self): lambda request: raise_runtime_error("boom"), ) try: - with pytest.raises(RuntimeError, match="callable failed"): + with pytest.raises(RuntimeError, match="RuntimeError: boom"): llm.conditional_execution(make_request()) finally: guardrails.deregister_llm_conditional_execution("py_llm_cond_error") diff --git a/python/tests/test_tools.py b/python/tests/test_tools.py index 3619902f3..a401eb73b 100644 --- a/python/tests/test_tools.py +++ b/python/tests/test_tools.py @@ -361,7 +361,7 @@ def test_duplicate_intercept_raises(self): def test_request_intercept_raises_on_exception(self): intercepts.register_tool_request("py_req_raise", 1, False, lambda n, a: raise_runtime_error("boom")) try: - with pytest.raises(RuntimeError, match="callable failed"): + with pytest.raises(RuntimeError, match="RuntimeError: boom"): tools.request_intercepts("raise_tool", {"value": 1}) finally: intercepts.deregister_tool_request("py_req_raise") @@ -374,7 +374,7 @@ def test_request_intercept_raises_on_unserializable_return(self): cast(intercepts.ToolRequestIntercept, lambda n, a: object()), ) try: - with pytest.raises(RuntimeError, match="py_to_json failed"): + with pytest.raises(RuntimeError, match="unsupported type object"): tools.request_intercepts("bad_return_tool", {"value": 1}) finally: intercepts.deregister_tool_request("py_req_bad_return") @@ -485,7 +485,7 @@ def test_conditional_execution_invalid_return_type_raises(self): cast(guardrails.ToolConditionalExecutionGuardrail, lambda name, args: 123), ) try: - with pytest.raises(RuntimeError, match="expected str or None"): + with pytest.raises(RuntimeError, match="unexpected type"): tools.conditional_execution("bad_type_tool", {}) finally: guardrails.deregister_tool_conditional_execution("py_cond_bad_type") @@ -497,7 +497,7 @@ def test_conditional_execution_callable_error_raises(self): lambda name, args: raise_runtime_error("boom"), ) try: - with pytest.raises(RuntimeError, match="callable failed"): + with pytest.raises(RuntimeError, match="RuntimeError: boom"): tools.conditional_execution("error_tool", {}) finally: guardrails.deregister_tool_conditional_execution("py_cond_error") From 48301e6e05bb13fd841c166d92984539021ac81c Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 15:18:14 -0400 Subject: [PATCH 15/83] test: preserve legacy FFI sanitizer errors Signed-off-by: Will Killian --- crates/ffi/tests/unit/callable_tests.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/crates/ffi/tests/unit/callable_tests.rs b/crates/ffi/tests/unit/callable_tests.rs index 60b260868..4b5a2d251 100644 --- a/crates/ffi/tests/unit/callable_tests.rs +++ b/crates/ffi/tests/unit/callable_tests.rs @@ -498,18 +498,14 @@ fn test_llm_sanitizers_fail_closed_for_runtime_codec_ids_with_embedded_nul() { let request_sanitizer = wrap_llm_sanitize_request_fn(llm_request_alias_cb, std::ptr::null_mut(), None); - let request_error = resolve(request_sanitizer( + let request_result = resolve(request_sanitizer( make_request(), nemo_relay::api::runtime::LlmSanitizeRequestContext::with_identity( runtime_identity.clone(), ), )) - .expect_err("an embedded runtime codec ID must fail the callback wrapper"); - assert!( - request_error - .to_string() - .contains("runtime codec ID contains an embedded NUL") - ); + .expect("legacy sanitizer wrappers report callback errors out of band"); + assert_eq!(request_result, None); assert!( last_error_message() .unwrap() @@ -518,16 +514,12 @@ fn test_llm_sanitizers_fail_closed_for_runtime_codec_ids_with_embedded_nul() { let response_sanitizer = wrap_llm_sanitize_response_fn(json_alias_cb, std::ptr::null_mut(), None); - let response_error = resolve(response_sanitizer( + let response_result = resolve(response_sanitizer( json!({"secret": "must be omitted"}), nemo_relay::api::runtime::LlmSanitizeResponseContext::with_identity(runtime_identity), )) - .expect_err("an embedded runtime codec ID must fail the callback wrapper"); - assert!( - response_error - .to_string() - .contains("runtime codec ID contains an embedded NUL") - ); + .expect("legacy sanitizer wrappers report callback errors out of band"); + assert_eq!(response_result, None); assert!( last_error_message() .unwrap() From f561f0165b52577749883fb5d71526f66fdd4004 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 15:49:53 -0400 Subject: [PATCH 16/83] fix: address async middleware review feedback Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 238 +++++++++--------- crates/core/src/api/runtime/callbacks.rs | 2 +- crates/core/src/api/runtime/state.rs | 3 +- .../src/api/runtime/subscriber_dispatcher.rs | 3 + crates/core/src/api/scope.rs | 8 +- crates/core/src/api/tool.rs | 86 ++++--- crates/core/src/plugin/dynamic/native.rs | 107 +++++--- crates/core/src/plugin/dynamic/worker.rs | 7 +- crates/core/src/stream.rs | 19 +- .../tests/fixtures/native_plugin/src/lib.rs | 6 + crates/core/tests/unit/native_plugin_tests.rs | 63 +++-- crates/ffi/src/api/mod.rs | 2 + crates/ffi/src/callable.rs | 8 +- .../tests/integration/callable_extra_tests.rs | 9 +- crates/ffi/tests/integration/main.rs | 2 + crates/ffi/tests/support/mod.rs | 14 ++ crates/ffi/tests/unit/callable_tests.rs | 17 +- crates/node/plugin.d.ts | 4 +- crates/node/src/callable.rs | 59 +++-- crates/node/src/callback_factory.rs | 8 +- crates/node/tests/llm_tests.mjs | 13 +- crates/node/tests/scope_tests.mjs | 7 +- crates/node/tests/tools_tests.mjs | 6 +- crates/pii-redaction/src/builtin.rs | 16 +- .../tests/unit/component_tests.rs | 32 +-- crates/python/src/py_api/mod.rs | 60 +++-- crates/python/src/py_callable.rs | 32 ++- .../tests/coverage/py_api_coverage_tests.rs | 48 ++++ .../coverage/py_callable_coverage_tests.rs | 61 ++++- docs/reference/migration-guides.mdx | 6 + 30 files changed, 596 insertions(+), 350 deletions(-) create mode 100644 crates/ffi/tests/support/mod.rs diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index 06ac5119b..d0bcc79a3 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -745,48 +745,47 @@ pub fn llm_call(params: LlmCallParams<'_>) -> Result { state.build_llm_start_event(&handle, None, None) }; let queued_handle = handle.clone(); - if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { - dispatch_transformed_event( - event, - Box::new(move |event| { - Box::pin(async move { - let mut sanitized_request = - NemoRelayContextState::llm_sanitize_request_snapshot_chain( - request.clone(), - LlmSanitizeRequestContext::default(), - &entries, - ) - .await; - let request_changed = sanitized_request - .as_ref() - .is_some_and(|sanitized| sanitized != &request); - let mut annotation = if sanitized_request.is_none() || request_changed { - None - } else { - annotated_request - }; - if !agent_is_fresh && let Some(sanitized_request) = sanitized_request.as_mut() { - project_llm_request_to_current_user_turn( - sanitized_request, - &mut annotation, - None, - ); - } - let input = sanitized_request - .as_ref() - .and_then(|request| serde_json::to_value(request).ok()); - let context = global_context(); - match context.read() { - Ok(state) => state.build_llm_start_event(&queued_handle, input, annotation), - Err(_) => event, - } - }) - }), - event_sanitizers, - &subscribers, - scope_stack, - ); - } + let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + dispatch_transformed_event( + event, + Box::new(move |event| { + Box::pin(async move { + let mut sanitized_request = + NemoRelayContextState::llm_sanitize_request_snapshot_chain( + request.clone(), + LlmSanitizeRequestContext::default(), + &entries, + ) + .await; + let request_changed = sanitized_request + .as_ref() + .is_some_and(|sanitized| sanitized != &request); + let mut annotation = if sanitized_request.is_none() || request_changed { + None + } else { + annotated_request + }; + if !agent_is_fresh && let Some(sanitized_request) = sanitized_request.as_mut() { + project_llm_request_to_current_user_turn( + sanitized_request, + &mut annotation, + None, + ); + } + let input = sanitized_request + .as_ref() + .and_then(|request| serde_json::to_value(request).ok()); + let context = global_context(); + match context.read() { + Ok(state) => state.build_llm_start_event(&queued_handle, input, annotation), + Err(_) => event, + } + }) + }), + event_sanitizers, + &subscribers, + scope_stack, + ); Ok(handle) } @@ -873,87 +872,84 @@ pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { .build(), ) }; - if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { - dispatch_transformed_event( - event, - Box::new(move |event| { - Box::pin(async move { - let sanitized = NemoRelayContextState::llm_sanitize_response_snapshot_chain( - response.clone(), - LlmSanitizeResponseContext::for_response_codec(response_codec.clone()), - &entries, - ) - .await; - let changed = sanitized - .as_ref() - .is_some_and(|sanitized| sanitized != &response); - let data = match sanitized { - Some(response) - if response_was_null_without_fallback && response.is_null() => - { - None - } - response => response, - }; - let annotation_omitted = data.as_ref().is_none_or(Json::is_null); - let (mut annotation, decode_error) = if annotation_omitted { - (None, None) - } else { - resolve_llm_end_annotation( - (!changed).then_some(annotated_response).flatten(), - response_codec, - data.as_ref(), - &LlmCallEndBehavior { - response_codec_errors_fatal: false, - attach_estimated_cost: false, - }, - &handle.name, - ) - }; - if let Some(error) = decode_error { - log::error!( - target: "nemo_relay.runtime", - event = "manual_llm_response_codec_failed"; - "Manual LLM response annotation failed during queued publication: {error}" - ); - } - let pricing = crate::codec::response::active_pricing_resolver(); - let summary = finalize_optimization_summary( - &handle.optimization_recorder, - annotation.as_mut(), - handle.model_name.as_deref(), - &pricing, - ); - if !annotation_omitted - && annotation.is_none() - && let Some(summary) = summary - { - annotation = Some(AnnotatedLlmResponse { - optimization_summary: Some(summary), - ..AnnotatedLlmResponse::default() - }); + let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + dispatch_transformed_event( + event, + Box::new(move |event| { + Box::pin(async move { + let sanitized = NemoRelayContextState::llm_sanitize_response_snapshot_chain( + response.clone(), + LlmSanitizeResponseContext::for_response_codec(response_codec.clone()), + &entries, + ) + .await; + let changed = sanitized + .as_ref() + .is_some_and(|sanitized| sanitized != &response); + let data = match sanitized { + Some(response) if response_was_null_without_fallback && response.is_null() => { + None } - let context = global_context(); - let Ok(state) = context.read() else { - return event; - }; - let end_metadata = metadata_with_otel_status(metadata, "OK", None); - state.build_llm_end_event( - EndLlmHandleParams::builder() - .handle(&handle) - .data_opt(data) - .metadata_opt(end_metadata) - .annotated_response_opt(annotation.map(Arc::new)) - .timestamp_opt(timestamp) - .build(), + response => response, + }; + let annotation_omitted = data.as_ref().is_none_or(Json::is_null); + let (mut annotation, decode_error) = if annotation_omitted { + (None, None) + } else { + resolve_llm_end_annotation( + (!changed).then_some(annotated_response).flatten(), + response_codec, + data.as_ref(), + &LlmCallEndBehavior { + response_codec_errors_fatal: false, + attach_estimated_cost: false, + }, + &handle.name, ) - }) - }), - event_sanitizers, - &subscribers, - scope_stack, - ); - } + }; + if let Some(error) = decode_error { + log::error!( + target: "nemo_relay.runtime", + event = "manual_llm_response_codec_failed"; + "Manual LLM response annotation failed during queued publication: {error}" + ); + } + let pricing = crate::codec::response::active_pricing_resolver(); + let summary = finalize_optimization_summary( + &handle.optimization_recorder, + annotation.as_mut(), + handle.model_name.as_deref(), + &pricing, + ); + if !annotation_omitted + && annotation.is_none() + && let Some(summary) = summary + { + annotation = Some(AnnotatedLlmResponse { + optimization_summary: Some(summary), + ..AnnotatedLlmResponse::default() + }); + } + let context = global_context(); + let Ok(state) = context.read() else { + return event; + }; + let end_metadata = metadata_with_otel_status(metadata, "OK", None); + state.build_llm_end_event( + EndLlmHandleParams::builder() + .handle(&handle) + .data_opt(data) + .metadata_opt(end_metadata) + .annotated_response_opt(annotation.map(Arc::new)) + .timestamp_opt(timestamp) + .build(), + ) + }) + }), + event_sanitizers, + &subscribers, + scope_stack, + ); Ok(()) } diff --git a/crates/core/src/api/runtime/callbacks.rs b/crates/core/src/api/runtime/callbacks.rs index b07a82fde..d52577f07 100644 --- a/crates/core/src/api/runtime/callbacks.rs +++ b/crates/core/src/api/runtime/callbacks.rs @@ -29,7 +29,7 @@ use crate::json::Json; /// it may replace. Later callbacks observe fields returned by earlier entries. pub type EventSanitizeFn = Arc< dyn Fn( - Event, + Arc, EventSanitizeFields, ) -> Pin> + Send>> + Send diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index e2098b9a7..a70d3a996 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -637,9 +637,10 @@ impl NemoRelayContextState { mut event: Event, entries: &[Guardrail], ) -> Event { + let event_context = Arc::new(event.clone()); for entry in entries { let fields = event.sanitize_fields(); - match (entry.payload)(event.clone(), fields).await { + match (entry.payload)(Arc::clone(&event_context), fields).await { Ok(fields) => event.apply_sanitize_fields(fields), Err(error) => log::error!( target: "nemo_relay.runtime", diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index bdd48d20f..475a0434c 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -121,6 +121,9 @@ mod native { subscribers: &[EventSubscriberFn], scope_stack: ScopeStackHandle, ) -> bool { + if subscribers.is_empty() { + return true; + } let message = DispatcherMessage::Deliver { event: Box::new(event), transform: Some(transform), diff --git a/crates/core/src/api/scope.rs b/crates/core/src/api/scope.rs index 24e5d8cc3..635c6c76c 100644 --- a/crates/core/src/api/scope.rs +++ b/crates/core/src/api/scope.rs @@ -217,8 +217,8 @@ pub fn get_handle() -> Result { /// cannot be read safely. /// /// # Notes -/// Scope-local subscribers attached to ancestor scopes observe the emitted -/// start event before the function returns. +/// The start event is queued with subscriber and sanitizer snapshots captured +/// while the new scope is active. pub fn push_scope(params: PushScopeParams<'_>) -> Result { ensure_runtime_owner()?; let parent_uuid = resolve_parent_uuid(params.parent); @@ -353,8 +353,8 @@ pub fn pop_scope(params: PopScopeParams<'_>) -> Result<()> { /// cannot be read safely. /// /// # Notes -/// Scope-local subscribers attached to ancestor scopes observe the emitted -/// mark event just like scope, tool, and LLM lifecycle events. +/// The mark event is queued with subscriber and sanitizer snapshots captured +/// from the active scope stack. pub fn event(params: EmitMarkEventParams<'_>) -> Result<()> { ensure_runtime_owner()?; let parent_uuid = resolve_parent_uuid(params.parent); diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 7d6d10f71..0c1128d39 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -281,26 +281,25 @@ pub fn tool_call(params: ToolCallParams<'_>) -> Result { (handle, event, marks) }; let tool_name = handle.name.clone(); - if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { - dispatch_transformed_event( - event, - Box::new(move |mut event| { - Box::pin(async move { - let sanitized = NemoRelayContextState::tool_sanitize_request_snapshot_chain( - &tool_name, raw_args, &entries, - ) - .await; - let mut fields = event.sanitize_fields(); - fields.data = Some(sanitized); - event.apply_sanitize_fields(fields); - event - }) - }), - event_sanitizers, - &subscribers, - scope_stack.clone(), - ); - } + let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + dispatch_transformed_event( + event, + Box::new(move |mut event| { + Box::pin(async move { + let sanitized = NemoRelayContextState::tool_sanitize_request_snapshot_chain( + &tool_name, raw_args, &entries, + ) + .await; + let mut fields = event.sanitize_fields(); + fields.data = Some(sanitized); + event.apply_sanitize_fields(fields); + event + }) + }), + event_sanitizers, + &subscribers, + scope_stack.clone(), + ); for mark in marks { if let Some(sanitizers) = snapshot_event_sanitizers(&mark, &scope_stack) { dispatch_sanitized_event(mark, sanitizers, &subscribers, scope_stack.clone()); @@ -463,30 +462,29 @@ pub fn tool_call_end(params: ToolCallEndParams<'_>) -> Result<()> { ) }; let tool_name = params.handle.name.clone(); - if let Some(event_sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) { - dispatch_transformed_event( - event, - Box::new(move |mut event| { - Box::pin(async move { - let sanitized = NemoRelayContextState::tool_sanitize_response_snapshot_chain( - &tool_name, result, &entries, - ) - .await; - let mut fields = event.sanitize_fields(); - fields.data = if sanitized.is_null() { - fallback - } else { - Some(sanitized) - }; - event.apply_sanitize_fields(fields); - event - }) - }), - event_sanitizers, - &subscribers, - scope_stack, - ); - } + let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + dispatch_transformed_event( + event, + Box::new(move |mut event| { + Box::pin(async move { + let sanitized = NemoRelayContextState::tool_sanitize_response_snapshot_chain( + &tool_name, result, &entries, + ) + .await; + let mut fields = event.sanitize_fields(); + fields.data = if sanitized.is_null() { + fallback + } else { + Some(sanitized) + }; + event.apply_sanitize_fields(fields); + event + }) + }), + event_sanitizers, + &subscribers, + scope_stack, + ); Ok(()) } diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 7481d86ee..084a8900b 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -1341,8 +1341,39 @@ fn make_user_data( } /// One-shot state retained by a v3 native async callback. +enum NativeAsyncResult { + Json(Json), + LlmStream(LlmJsonStream), +} + +impl std::fmt::Debug for NativeAsyncResult { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Json(value) => formatter.debug_tuple("Json").field(value).finish(), + Self::LlmStream(_) => formatter.write_str("LlmStream(..)"), + } + } +} + +impl PartialEq for NativeAsyncResult { + fn eq(&self, other: &Json) -> bool { + matches!(self, Self::Json(value) if value == other) + } +} + +impl NativeAsyncResult { + fn into_json(self) -> FlowResult { + match self { + Self::Json(value) => Ok(value), + Self::LlmStream(_) => Err(FlowError::Internal( + "native async callback returned a stream for a non-stream invocation".into(), + )), + } + } +} + struct NativeAsyncCompletion { - sender: Mutex>>>, + sender: Mutex>>>, cancelled: AtomicBool, // A pending native callback can continue running after its completion // wakes the awaiting task. Keep the callback's dynamic-library instance @@ -1352,7 +1383,7 @@ struct NativeAsyncCompletion { struct NativeAsyncWait { completion: Arc, - receiver: tokio::sync::oneshot::Receiver>, + receiver: tokio::sync::oneshot::Receiver>, } impl Drop for NativeAsyncWait { @@ -1380,7 +1411,7 @@ async fn invoke_native_async_callback( user_data: Arc, invocation: Json, next: Option, -) -> FlowResult { +) -> FlowResult { let runtime = if next.is_some() { Some(tokio::runtime::Handle::try_current().map_err(|error| { FlowError::Internal(format!( @@ -1486,7 +1517,7 @@ unsafe extern "C" fn native_async_completion_resolve_json( else { return NemoRelayStatus::InvalidArg; }; - let _ = sender.send(Ok(value)); + let _ = sender.send(Ok(NativeAsyncResult::Json(value))); NemoRelayStatus::Ok } @@ -1563,11 +1594,14 @@ unsafe extern "C" fn native_async_next_invoke( }; unsafe { Arc::increment_strong_count(completion as *const NativeAsyncCompletion) }; let completion = unsafe { Arc::from_raw(completion as *const NativeAsyncCompletion) }; - let future: Pin> + Send>> = match &next.inner { + let future: Pin> + Send>> = match &next + .inner + { NativeAsyncNextInner::Tool(next) => { let next = next.clone(); Box::pin(async move { serde_json::to_value(ToolExecutionInterceptOutcome::new(next(invocation).await?)) + .map(NativeAsyncResult::Json) .map_err(|error| { FlowError::Internal(format!( "failed to serialize native async tool outcome: {error}" @@ -1589,7 +1623,7 @@ unsafe extern "C" fn native_async_next_invoke( } }; let next = next.clone(); - Box::pin(async move { next(request).await }) + Box::pin(async move { next(request).await.map(NativeAsyncResult::Json) }) } NativeAsyncNextInner::LlmStream(next) => { let request = match serde_json::from_value(invocation) { @@ -1605,14 +1639,7 @@ unsafe extern "C" fn native_async_next_invoke( } }; let next = next.clone(); - Box::pin(async move { - let mut stream = next(request).await?; - let mut chunks = Vec::new(); - while let Some(chunk) = stream.next().await { - chunks.push(chunk?); - } - Ok(Json::Array(chunks)) - }) + Box::pin(async move { next(request).await.map(NativeAsyncResult::LlmStream) }) } }; next.runtime.spawn(async move { @@ -1645,7 +1672,8 @@ fn wrap_native_async_tool_json( serde_json::json!({"name": name, "value": value}), None, ) - .await?; + .await? + .into_json()?; Ok(value) }) }) @@ -1668,6 +1696,7 @@ fn wrap_native_async_tool_conditional( None, ) .await? + .into_json()? { Json::Null => Ok(None), Json::String(reason) => Ok(Some(reason)), @@ -1696,6 +1725,7 @@ fn wrap_native_async_llm_conditional( None, ) .await? + .into_json()? { Json::Null => Ok(None), Json::String(reason) => Ok(Some(reason)), @@ -1724,7 +1754,8 @@ fn wrap_native_async_llm_sanitize_request( serde_json::json!({"request": request, "context": {"codec": codec}}), None, ) - .await?; + .await? + .into_json()?; if value.is_null() { Ok(None) } else { @@ -1753,7 +1784,8 @@ fn wrap_native_async_llm_sanitize_response( serde_json::json!({"response": response, "context": {"codec": codec}}), None, ) - .await?; + .await? + .into_json()?; Ok((!value.is_null()).then_some(value)) }) }) @@ -1780,7 +1812,8 @@ fn wrap_native_async_llm_request_intercept( }), None, ) - .await?, + .await? + .into_json()?, ) .map_err(|error| { FlowError::Internal(format!( @@ -1808,7 +1841,8 @@ fn wrap_native_async_event_sanitize( serde_json::json!({"event": event, "fields": fields}), None, ) - .await?, + .await? + .into_json()?, ) .map_err(|error| { FlowError::Internal(format!("invalid native async event fields: {error}")) @@ -1835,7 +1869,8 @@ fn wrap_native_async_tool_execution( invocation, Some(NativeAsyncNextInner::Tool(next)), ) - .await?, + .await? + .into_json()?, ) .map_err(|error| { FlowError::Internal(format!("invalid native async tool outcome: {error}")) @@ -1853,12 +1888,17 @@ fn wrap_native_async_llm_execution( let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |name, request, next| { let user_data = user_data.clone(); - Box::pin(invoke_native_async_callback( - cb, - user_data, - serde_json::json!({"name": name, "request": request}), - Some(NativeAsyncNextInner::Llm(next)), - )) + let name = name.to_owned(); + Box::pin(async move { + invoke_native_async_callback( + cb, + user_data, + serde_json::json!({"name": name, "request": request}), + Some(NativeAsyncNextInner::Llm(next)), + ) + .await? + .into_json() + }) }) } @@ -1880,14 +1920,15 @@ fn wrap_native_async_llm_stream_execution( Some(NativeAsyncNextInner::LlmStream(next)), ) .await?; - let chunks = value.as_array().cloned().ok_or_else(|| { - FlowError::Internal( + match value { + NativeAsyncResult::LlmStream(stream) => Ok(stream), + NativeAsyncResult::Json(Json::Array(chunks)) => Ok(LlmJsonStream::new( + tokio_stream::iter(chunks.into_iter().map(Ok)), + )), + NativeAsyncResult::Json(_) => Err(FlowError::Internal( "native async LLM stream intercept must resolve to an array".into(), - ) - })?; - Ok(LlmJsonStream::new(tokio_stream::iter( - chunks.into_iter().map(Ok), - ))) + )), + } }) }) } diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 5262dd784..1d0bb2ecf 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -1120,7 +1120,7 @@ impl WorkerPluginInstance { let instance = Arc::new(self.clone_for_callback()); let callback_name = name.to_owned(); let callback: EventSanitizeFn = - Arc::new(move |event: Event, _fields: EventSanitizeFields| { + Arc::new(move |event: Arc, _fields: EventSanitizeFields| { let instance = instance.clone(); let callback_name = callback_name.clone(); Box::pin(async move { @@ -1875,12 +1875,15 @@ impl WorkerPluginCallback { .invoke_async_with_timeout(request, WORKER_RPC_TIMEOUT) .await; if let Err(error) = &result { + let surface_name = RegistrationSurface::try_from(surface) + .map(|surface| surface.as_str_name()) + .unwrap_or("UNKNOWN"); log::warn!( target: "nemo_relay.worker", event = "worker_callback_failed", plugin_id = self.plugin_kind.as_str(), callback = callback_name.as_str(), - surface; + surface = surface_name; "Worker plugin callback failed: {error}" ); } diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index 756f066a7..2e742fea3 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -344,12 +344,14 @@ impl LlmStreamWrapper { match tokio::runtime::Handle::try_current() { Ok(handle) => Some(handle.spawn(finalize)), Err(_) => { - if let Ok(runtime) = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - runtime.block_on(finalize); - } + std::thread::spawn(move || { + if let Ok(runtime) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + runtime.block_on(finalize); + } + }); None } } @@ -420,7 +422,10 @@ impl Stream for LlmStreamWrapper { } if this.ended { - return Poll::Ready(None); + return match this.terminal_result.take() { + Some(result) => Poll::Ready(Some(result)), + None => Poll::Ready(None), + }; } // Poll the inner stream diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index acade0d0c..1367f4d74 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -797,6 +797,9 @@ unsafe extern "C" fn raw_async_tool_execution_callback( }; if next.is_null() || completion.is_null() { unsafe { reject_async_completion(host, completion, "async tool execution requires next and completion") }; + if !next.is_null() { + unsafe { (host.async_next_release)(next) }; + } return NemoRelayNativeAsyncCallbackState::Complete; } let value = unsafe { raw_host_string_value(&host.v1, invocation_json) } @@ -812,11 +815,13 @@ unsafe extern "C" fn raw_async_tool_execution_callback( .and_then(|value| serde_json::to_string(&value).ok()); let Some(value) = value else { unsafe { reject_async_completion(host, completion, "invalid async tool execution invocation") }; + unsafe { (host.async_next_release)(next) }; return NemoRelayNativeAsyncCallbackState::Complete; }; let value = unsafe { raw_host_string(&host.v1, &value) }; if value.is_null() { unsafe { reject_async_completion(host, completion, "failed to allocate async tool execution invocation") }; + unsafe { (host.async_next_release)(next) }; return NemoRelayNativeAsyncCallbackState::Complete; } let status = unsafe { (host.async_next_invoke)(next, value, completion) }; @@ -831,6 +836,7 @@ unsafe extern "C" fn raw_async_tool_execution_callback( NemoRelayNativeAsyncCallbackState::Pending } else { unsafe { reject_async_completion(host, completion, "failed to invoke async tool execution next") }; + unsafe { (host.async_next_release)(next) }; NemoRelayNativeAsyncCallbackState::Complete } } diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index a29955c17..934e0c3ee 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -284,22 +284,6 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { .unwrap(), json!({"llm": true}), ), - ( - NativeAsyncNextInner::LlmStream(Arc::new(|_request| { - Box::pin(async { - Ok(LlmJsonStream::new(tokio_stream::iter(vec![ - Ok(json!({"chunk": 1})), - Ok(json!({"chunk": 2})), - ]))) - }) - })), - serde_json::to_value(LlmRequest { - headers: Map::new(), - content: json!({"stream": true}), - }) - .unwrap(), - json!([{"chunk": 1}, {"chunk": 2}]), - ), ]; for (inner, invocation, expected) in cases { @@ -329,6 +313,53 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { native_async_completion_release(completion_ref); } } + + let next = Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + Box::pin(async { + Ok(LlmJsonStream::new(tokio_stream::iter(vec![ + Ok(json!({"chunk": 1})), + Ok(json!({"chunk": 2})), + ]))) + }) + })), + runtime: runtime.handle().clone(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"stream": true}), + }) + .unwrap(), + ) + .unwrap(); + assert_eq!( + unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, + NemoRelayStatus::Ok + ); + let NativeAsyncResult::LlmStream(mut stream) = runtime.block_on(receiver).unwrap().unwrap() + else { + panic!("stream continuation should preserve the downstream stream"); + }; + assert_eq!( + runtime.block_on(stream.next()).unwrap().unwrap(), + json!({"chunk": 1}) + ); + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_completion_release(completion_ref); + } } #[test] diff --git a/crates/ffi/src/api/mod.rs b/crates/ffi/src/api/mod.rs index f2984f32e..1fc8977fd 100644 --- a/crates/ffi/src/api/mod.rs +++ b/crates/ffi/src/api/mod.rs @@ -100,6 +100,8 @@ fn tokio_runtime() -> &'static Runtime { } fn block_on_sync_ffi(future: impl Future>) -> FlowResult { + // Embedded hosts must not call synchronous middleware helpers from a Tokio + // runtime thread. Use the completion-based async registration API there. if tokio::runtime::Handle::try_current().is_ok() { return Err(nemo_relay::error::FlowError::Internal( "synchronous FFI middleware helpers cannot run on a Tokio runtime thread; use the completion-based async registration API".into(), diff --git a/crates/ffi/src/callable.rs b/crates/ffi/src/callable.rs index f1c952863..e876cbe34 100644 --- a/crates/ffi/src/callable.rs +++ b/crates/ffi/src/callable.rs @@ -939,10 +939,10 @@ pub fn wrap_event_sanitize_fn( free_fn: NemoRelayFreeFn, ) -> EventSanitizeFn { let ud = make_user_data(user_data, free_fn); - Arc::new(move |event: Event, fields: EventSanitizeFields| { + Arc::new(move |event: Arc, fields: EventSanitizeFields| { let ud = ud.clone(); Box::pin(async move { - let ffi_event = FfiEvent(event); + let ffi_event = FfiEvent((*event).clone()); let fields_json = json_to_c_string(&serde_json::to_value(&fields).unwrap_or(Json::Null)); let result_ptr = unsafe { cb(ud.ptr, &ffi_event, fields_json) }; @@ -1071,6 +1071,10 @@ unsafe fn nemo_relay_string_free_internal(ptr: *mut c_char) { } } +#[cfg(test)] +#[path = "../tests/support/mod.rs"] +mod test_support; + #[cfg(test)] #[path = "../tests/unit/callable_tests.rs"] mod tests; diff --git a/crates/ffi/tests/integration/callable_extra_tests.rs b/crates/ffi/tests/integration/callable_extra_tests.rs index 69a341b96..2a284f07e 100644 --- a/crates/ffi/tests/integration/callable_extra_tests.rs +++ b/crates/ffi/tests/integration/callable_extra_tests.rs @@ -4,18 +4,11 @@ //! Integration tests for callable extra in the NeMo Relay FFI crate. use super::*; -use std::future::Future; use std::ptr; use tokio_stream::StreamExt; -fn resolve(future: impl Future) -> T { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(future) -} +use super::test_support::resolve; unsafe extern "C" fn tool_conditional_error_cb( _user_data: *mut libc::c_void, diff --git a/crates/ffi/tests/integration/main.rs b/crates/ffi/tests/integration/main.rs index baed8d570..e8f3035f4 100644 --- a/crates/ffi/tests/integration/main.rs +++ b/crates/ffi/tests/integration/main.rs @@ -36,5 +36,7 @@ mod convert_coverage_tests; #[path = "../coverage/error_tests.rs"] mod error_coverage_tests; mod plugin_activation_tests; +#[path = "../support/mod.rs"] +mod test_support; #[path = "../unit/types_tests.rs"] mod types_tests; diff --git a/crates/ffi/tests/support/mod.rs b/crates/ffi/tests/support/mod.rs new file mode 100644 index 000000000..8e557b330 --- /dev/null +++ b/crates/ffi/tests/support/mod.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shared helpers for FFI tests. + +use std::future::Future; + +pub(crate) fn resolve(future: impl Future) -> T { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(future) +} diff --git a/crates/ffi/tests/unit/callable_tests.rs b/crates/ffi/tests/unit/callable_tests.rs index 4b5a2d251..1ae03a985 100644 --- a/crates/ffi/tests/unit/callable_tests.rs +++ b/crates/ffi/tests/unit/callable_tests.rs @@ -4,7 +4,6 @@ //! Unit tests for callable in the NeMo Relay FFI crate. use super::*; -use std::future::Future; use std::sync::atomic::{AtomicUsize, Ordering}; use nemo_relay::api::event::{Event, EventSanitizeFields}; @@ -12,6 +11,8 @@ use nemo_relay::api::llm::{LlmAttributes, LlmHandle}; use serde_json::json; use tokio_stream::StreamExt; +use super::test_support::resolve; + extern "C" fn free_arc_counter(user_data: *mut libc::c_void) { let counter = unsafe { Box::from_raw(user_data as *mut Arc) }; counter.fetch_add(1, Ordering::SeqCst); @@ -23,14 +24,6 @@ fn user_data_counter() -> (*mut libc::c_void, Arc) { (ptr, counter) } -fn resolve(future: impl Future) -> T { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(future) -} - unsafe extern "C" fn tool_sanitize_cb( user_data: *mut libc::c_void, name: *const c_char, @@ -668,7 +661,7 @@ fn test_wrap_llm_exec_stream_and_event_callbacks() { .build(); let (user_data, sanitize_calls) = user_data_counter(); let sanitizer = wrap_event_sanitize_fn(event_sanitize_cb, user_data, Some(free_arc_counter)); - let sanitized = resolve(sanitizer(event.clone(), original_fields.clone())).unwrap(); + let sanitized = resolve(sanitizer(Arc::new(event.clone()), original_fields.clone())).unwrap(); assert_eq!(sanitized.data, Some(json!({"safe": true}))); assert_eq!( sanitized @@ -684,12 +677,12 @@ fn test_wrap_llm_exec_stream_and_event_callbacks() { let invalid = wrap_event_sanitize_fn(invalid_event_sanitize_cb, std::ptr::null_mut(), None); assert_eq!( - resolve(invalid(event.clone(), original_fields.clone())).unwrap(), + resolve(invalid(Arc::new(event.clone()), original_fields.clone())).unwrap(), EventSanitizeFields::default() ); let null = wrap_event_sanitize_fn(null_event_sanitize_cb, std::ptr::null_mut(), None); assert_eq!( - resolve(null(event, original_fields.clone())).unwrap(), + resolve(null(Arc::new(event), original_fields.clone())).unwrap(), EventSanitizeFields::default() ); diff --git a/crates/node/plugin.d.ts b/crates/node/plugin.d.ts index 158e25b2f..cc0a130b3 100644 --- a/crates/node/plugin.d.ts +++ b/crates/node/plugin.d.ts @@ -225,7 +225,7 @@ export interface PluginContext { registerToolConditionalExecutionGuardrail( name: string, priority: number, - callback: (name: string, args: Json) => string | null, + callback: (name: string, args: Json) => string | null | Promise, ): void; /** Register an LLM sanitize-request guardrail. The callback receives `(request, context)`. */ registerLlmSanitizeRequestGuardrail( @@ -272,7 +272,7 @@ export interface PluginContext { name: string, priority: number, breakChain: boolean, - callback: (name: string, args: Json) => Json, + callback: (name: string, args: Json) => Json | Promise, ): void; /** * Register tool execution middleware that returns a canonical outcome. diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index b40aba073..c2ef47490 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -300,7 +300,13 @@ pub fn wrap_js_llm_sanitize_request_promise_fn(func: Arc) -> Llm move |request: LlmRequest, context: LlmSanitizeRequestContext| { let func = func.clone(); Box::pin(async move { - let request = serde_json::to_value(request).unwrap_or(Json::Null); + let request = serde_json::to_value(request).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM sanitize request: {error}" + )); + record_callback_error(error.to_string()); + error + })?; let context = js_llm_sanitize_request_context(&context); let value = func .call_spread_with_arg0(Box::new(move |env| { @@ -375,8 +381,15 @@ pub fn wrap_js_llm_conditional_promise_fn(func: Arc) -> LlmCondi Arc::new(move |request: LlmRequest| { let func = func.clone(); Box::pin(async move { + let request = serde_json::to_value(request).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM conditional request: {error}" + )); + record_callback_error(error.to_string()); + error + })?; let value = func - .call(serde_json::to_value(request).unwrap_or(Json::Null)) + .call(request) .await .inspect_err(|error| record_callback_error(error.to_string()))?; match value { @@ -402,16 +415,28 @@ pub fn wrap_js_llm_request_intercept_promise_fn( move |name: String, request: LlmRequest, annotated: Option| { let func = func.clone(); Box::pin(async move { - let value = func - .call(serde_json::json!({ - "name": name, - "request": request, - "annotated": annotated, - })) - .await - .inspect_err(|error| { - record_callback_error(error.to_string()); - })?; + let request = serde_json::to_value(request).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM request intercept request: {error}" + )); + record_callback_error(error.to_string()); + error + })?; + let annotated = serde_json::to_value(annotated).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM request intercept annotation: {error}" + )); + record_callback_error(error.to_string()); + error + })?; + let value = serde_json::json!({ + "name": name, + "request": request, + "annotated": annotated, + }); + let value = func.call(value).await.inspect_err(|error| { + record_callback_error(error.to_string()); + })?; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct JsOutcome { @@ -448,7 +473,7 @@ pub fn wrap_js_llm_request_intercept_promise_fn( /// scope/mark APIs while allowing the JavaScript callback to settle a Promise /// on the Node event loop. pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSanitizeFn { - Arc::new(move |event: Event, fields: CoreEventSanitizeFields| { + Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); Box::pin(async move { let event_json = JsEvent::try_from_event(&event) @@ -753,7 +778,7 @@ pub fn wrap_js_llm_sanitize_request_fn( })?; let (tx, rx) = tokio::sync::oneshot::channel(); if func.call_with_return_value( - (request.clone(), context), + (request, context), ThreadsafeFunctionCallMode::Blocking, move |value: Option| { let _ = tx.send(callback_json(value)); @@ -1112,7 +1137,7 @@ pub fn wrap_js_event_sanitize_fn( func: ThreadsafeFunction<(Json, Json), ErrorStrategy::Fatal>, ) -> EventSanitizeFn { let func = Arc::new(func); - Arc::new(move |event: Event, fields: CoreEventSanitizeFields| { + Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); Box::pin(async move { let event_json = match JsEvent::try_from_event(&event) { @@ -1125,7 +1150,7 @@ pub fn wrap_js_event_sanitize_fn( } }; let js_fields = EventSanitizeFields { - data: fields.data.clone(), + data: fields.data, category_profile: fields .category_profile .as_ref() @@ -1138,7 +1163,7 @@ pub fn wrap_js_event_sanitize_fn( record_callback_error(error.to_string()); error })?, - metadata: fields.metadata.clone(), + metadata: fields.metadata, }; let js_fields = serde_json::to_value(js_fields).map_err(|error| { let error = FlowError::Internal(format!( diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index 48b301883..5fcd45ed8 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -68,7 +68,11 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { promise(fn) { return function __nemo_relay_promise_wrapper(error, arg0, spread, next, resolve, reject) { if (error != null) { - reject(error); + let message = 'unknown error'; + try { + message = String(error?.message ?? error); + } catch {} + reject(message); return; } Promise.resolve().then(() => ( @@ -80,6 +84,8 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { try { if (typeof error === 'string') { message = error; + } else if (error === null || (typeof error !== 'object' && typeof error !== 'function')) { + message = String(error); } else if (error != null && typeof error.message === 'string') { message = error.message; } diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index c83c78cb9..59f63ba33 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -58,14 +58,15 @@ async function flushSubscriberCallbacks() { } async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { - flushSubscribers(); const deadline = Date.now() + timeoutMs; while (!predicate()) { + await flushSubscribers(); if (Date.now() >= deadline) { throw new Error('timed out waiting for subscriber callbacks'); } await new Promise((resolve) => setImmediate(resolve)); } + await flushSubscribers(); } function makeNative() { @@ -764,7 +765,7 @@ describe('LLM guardrails', () => { event.scope_category === 'start', ); assert.deepEqual(start.data, { headers: request.headers, content: request.content }); - assert.match(getLastCallbackError() ?? '', /(unknown error|callback)/i); + assert.equal(getLastCallbackError(), 'internal error: unknown error'); deregisterLlmSanitizeRequestGuardrail('node_llm_san_req_throw'); const result = await llmCallExecute( @@ -1197,7 +1198,7 @@ describe('LLM intercepts', () => { deregisterLlmExecutionIntercept('node_llm_exec_invalid_next'); }); - it('execution intercept propagates primitive rejection values as unknown error', async () => { + it('execution intercept preserves primitive rejection values', async () => { registerLlmExecutionIntercept('node_llm_exec_unknown_err', 10, async () => { return rejectWith(42); }); @@ -1216,14 +1217,14 @@ describe('LLM intercepts', () => { null, null, ), - /unknown error/i, + /internal error: 42/i, ); } finally { deregisterLlmExecutionIntercept('node_llm_exec_unknown_err'); } }); - it('async execute falls back to unknown error for primitive rejections', async () => { + it('async execute preserves primitive rejection values', async () => { await assert.rejects( () => llmCallExecuteAsync( @@ -1236,7 +1237,7 @@ describe('LLM intercepts', () => { null, null, ), - /unknown error/i, + /internal error: 42/i, ); }); diff --git a/crates/node/tests/scope_tests.mjs b/crates/node/tests/scope_tests.mjs index 9ecd3ebb2..0332790ac 100644 --- a/crates/node/tests/scope_tests.mjs +++ b/crates/node/tests/scope_tests.mjs @@ -30,14 +30,15 @@ function rejectWithPrimitive(value) { } async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { - await flushSubscribers(); const deadline = Date.now() + timeoutMs; while (!predicate()) { + await flushSubscribers(); if (Date.now() >= deadline) { throw new Error('timed out waiting for subscriber callbacks'); } await new Promise((resolve) => setImmediate(resolve)); } + await flushSubscribers(); } // =========================================================================== @@ -277,14 +278,14 @@ describe('withScope', () => { } }); - it('surfaces primitive rejection values as unknown error and still pops the scope', async () => { + it('surfaces primitive rejection values and still pops the scope', async () => { const before = getHandle(); await assert.rejects( () => withScope('primitive_reject_test', ScopeType.Tool, async () => { return rejectWithPrimitive(123); }), - /unknown error/i, + /internal error: 123/i, ); const after = getHandle(); assert.equal(after.uuid, before.uuid, 'scope should be popped after primitive rejection'); diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index b60642aef..e104d0611 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -55,11 +55,13 @@ async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { // callback state is ready, with a timeout to avoid hanging the test forever. const deadline = Date.now() + timeoutMs; while (!predicate()) { + await flushSubscribers(); if (Date.now() >= deadline) { throw new Error('timed out waiting for subscriber callbacks'); } await new Promise((resolve) => setImmediate(resolve)); } + await flushSubscribers(); } // =========================================================================== @@ -996,7 +998,7 @@ describe('Tool intercepts', () => { } }); - it('async execute falls back to unknown error for primitive rejections', async () => { + it('async execute preserves primitive rejection values', async () => { await assert.rejects( () => toolCallExecuteAsync( @@ -1010,7 +1012,7 @@ describe('Tool intercepts', () => { null, null, ), - /unknown error/i, + /internal error: 42/i, ); }); diff --git a/crates/pii-redaction/src/builtin.rs b/crates/pii-redaction/src/builtin.rs index 71d421588..8852f8543 100644 --- a/crates/pii-redaction/src/builtin.rs +++ b/crates/pii-redaction/src/builtin.rs @@ -461,8 +461,9 @@ impl CompiledBuiltinBackend { } pub(super) fn tool_sanitize_callback(backend: CompiledBuiltinBackend) -> ToolSanitizeFn { + let backend = Arc::new(backend); Arc::new(move |_name: String, payload: Json| { - let backend = backend.clone(); + let backend = Arc::clone(&backend); Box::pin(async move { Ok(match backend.trajectory.as_ref() { Some(trajectory) => trajectory.sanitize_tool_payload(payload), @@ -488,11 +489,12 @@ fn event_sanitize_callback_with_scope_categories( backend: CompiledBuiltinBackend, scope_categories: Option<(bool, bool)>, ) -> EventSanitizeFn { + let backend = Arc::new(backend); Arc::new(move |event, mut fields| { - let backend = backend.clone(); + let backend = Arc::clone(&backend); Box::pin(async move { if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { - matches!(event, Event::Scope(_)) + matches!(event.as_ref(), Event::Scope(_)) && event .category() .is_some_and(|category| match category.as_str() { @@ -507,7 +509,7 @@ fn event_sanitize_callback_with_scope_categories( if let Some(trajectory) = backend.trajectory.as_ref() { return Ok(trajectory.sanitize_event_fields(&event, fields)); } - let specialized_scope = matches!(event, Event::Scope(_)) + let specialized_scope = matches!(event.as_ref(), Event::Scope(_)) && event .category() .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); @@ -532,8 +534,9 @@ fn event_sanitize_callback_with_scope_categories( pub(super) fn llm_sanitize_request_callback( backend: CompiledBuiltinBackend, ) -> LlmSanitizeRequestFn { + let backend = Arc::new(backend); Arc::new(move |mut request: LlmRequest, context| { - let backend = backend.clone(); + let backend = Arc::clone(&backend); Box::pin(async move { if let Some(trajectory) = backend.trajectory.as_ref() { request.headers = trajectory @@ -577,8 +580,9 @@ pub(super) fn llm_sanitize_request_callback( pub(super) fn llm_sanitize_response_callback( backend: CompiledBuiltinBackend, ) -> LlmSanitizeResponseFn { + let backend = Arc::new(backend); Arc::new(move |payload: Json, context| { - let backend = backend.clone(); + let backend = Arc::clone(&backend); Box::pin(async move { if let Some(trajectory) = backend.trajectory.as_ref() { return Ok(Some(trajectory.sanitize_provider_payload(payload))); diff --git a/crates/pii-redaction/tests/unit/component_tests.rs b/crates/pii-redaction/tests/unit/component_tests.rs index 2152c5d6d..2e80f1172 100644 --- a/crates/pii-redaction/tests/unit/component_tests.rs +++ b/crates/pii-redaction/tests/unit/component_tests.rs @@ -788,7 +788,7 @@ async fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { Some(CategoryProfile::builder().subtype("llm.chunk").build()), )); let sanitized = callback( - chunk.clone(), + Arc::new(chunk.clone()), EventSanitizeFields { data: Some(json!({ "chunk_index": 2, @@ -823,7 +823,7 @@ async fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { ), )); let sanitized = callback( - optimization.clone(), + Arc::new(optimization.clone()), EventSanitizeFields { data: Some(json!({ "producer": "neutral.router", @@ -867,7 +867,7 @@ async fn trajectory_preset_redacts_known_marks_and_nested_scope_content() { None, )); let sanitized = callback( - nested_agent, + Arc::new(nested_agent), EventSanitizeFields { data: Some(json!({ "request_id": "request-1", @@ -962,7 +962,7 @@ async fn trajectory_preset_preserves_trusted_scope_metadata_only() { None, )); let sanitized = callback( - event, + Arc::new(event), EventSanitizeFields { data: None, category_profile: None, @@ -982,7 +982,7 @@ async fn trajectory_preset_preserves_trusted_scope_metadata_only() { None, )); let sanitized = callback( - malformed, + Arc::new(malformed), EventSanitizeFields { data: None, category_profile: None, @@ -1012,7 +1012,7 @@ async fn trajectory_preset_preserves_trusted_scope_metadata_only() { Some(CategoryProfile::builder().subtype("llm.chunk").build()), )); let sanitized = callback( - mark.clone(), + Arc::new(mark.clone()), EventSanitizeFields { data: None, category_profile: mark.category_profile().cloned(), @@ -1046,13 +1046,15 @@ async fn trajectory_custom_mark_policy_is_explicit_and_shape_preserving() { let preserve = crate::builtin::event_sanitize_callback(trajectory_backend(None, "preserve")); assert_eq!( - preserve(event.clone(), fields.clone()).await.unwrap(), + preserve(Arc::new(event.clone()), fields.clone()) + .await + .unwrap(), fields ); let redact = crate::builtin::event_sanitize_callback(trajectory_backend(None, "redact_all_leaves")); - let sanitized = redact(event, fields).await.unwrap(); + let sanitized = redact(Arc::new(event), fields).await.unwrap(); assert_eq!( sanitized.data.unwrap(), json!({ @@ -1117,7 +1119,7 @@ async fn trajectory_profile_preserves_typed_llm_accounting_while_redacting_annot None, )); let sanitized = callback( - event, + Arc::new(event), EventSanitizeFields { data: Some(json!({"already": "sanitized by the response callback"})), category_profile: Some( @@ -1192,8 +1194,8 @@ async fn preserved_custom_marks_remain_eligible_for_a_later_email_profile() { .unwrap(), ); - let fields = trajectory(event.clone(), fields).await.unwrap(); - let sanitized = email(event, fields).await.unwrap(); + let fields = trajectory(Arc::new(event.clone()), fields).await.unwrap(); + let sanitized = email(Arc::new(event), fields).await.unwrap(); assert_eq!(sanitized.data.as_ref().unwrap()["owner"], "[REDACTED]"); assert_eq!(sanitized.data.as_ref().unwrap()["score"], 0.9); assert_eq!( @@ -1505,7 +1507,7 @@ async fn event_sanitizer_transforms_data_category_profile_and_metadata_independe None, )); let sanitized = callback( - event, + Arc::new(event), EventSanitizeFields { data: Some(json!({"email": "person@example.com"})), category_profile: Some( @@ -1551,7 +1553,7 @@ async fn llm_and_tool_scope_metadata_is_sanitized_without_reprocessing_typed_fie .subtype("person@example.com") .build(); let sanitized = callback( - event, + Arc::new(event), EventSanitizeFields { data: Some(json!({"content": "person@example.com"})), category_profile: Some(original_profile.clone()), @@ -1607,7 +1609,7 @@ async fn scope_event_sanitizer_respects_enabled_llm_and_tool_surfaces() { .subtype("person@example.com") .build(); let sanitized = callback( - event, + Arc::new(event), EventSanitizeFields { data: Some(json!({"content": "person@example.com"})), category_profile: Some(original_profile.clone()), @@ -1643,7 +1645,7 @@ async fn event_sanitizer_discards_category_profile_when_sanitization_fails() { None, )); let sanitized = callback( - event, + Arc::new(event), EventSanitizeFields { data: None, category_profile: Some(CategoryProfile { diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 80b9d672e..bf6280c04 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1327,14 +1327,13 @@ fn tool_request_intercepts<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; - let result = runtime - .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { - core_tool_api::tool_request_intercepts(&name, args_json).await - })) + let result = pyo3_async_runtimes::tokio::get_runtime() + .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_tool_api::tool_request_intercepts(&name, args_json).await + }), + )) .map_err(to_py_err)?; return json_to_py(py, &result).map(|value| value.into_bound(py)); } @@ -1371,14 +1370,13 @@ fn tool_conditional_execution<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; - runtime - .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { - core_tool_api::tool_conditional_execution(&name, &args_json).await - })) + pyo3_async_runtimes::tokio::get_runtime() + .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_tool_api::tool_conditional_execution(&name, &args_json).await + }), + )) .map_err(to_py_err)?; return Ok(py.None().into_bound(py)); } @@ -1415,14 +1413,13 @@ fn llm_request_intercepts<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; - let result = runtime - .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { - core_llm_api::llm_request_intercepts(&name, request.inner).await - })) + let result = pyo3_async_runtimes::tokio::get_runtime() + .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_llm_api::llm_request_intercepts(&name, request.inner).await + }), + )) .map_err(to_py_err)?; return Py::new( py, @@ -1460,14 +1457,13 @@ fn llm_conditional_execution<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| to_py_err(FlowError::Internal(error.to_string())))?; - runtime - .block_on(TASK_SCOPE_STACK.scope(scope_stack, async move { - core_llm_api::llm_conditional_execution(&request.inner).await - })) + pyo3_async_runtimes::tokio::get_runtime() + .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_llm_api::llm_conditional_execution(&request.inner).await + }), + )) .map_err(to_py_err)?; return Ok(py.None().into_bound(py)); } diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index f5cb02e55..71d423e49 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -53,6 +53,23 @@ use crate::py_types::{ type PyValueFuture = Pin>> + Send>>; +tokio::task_local! { + pub(crate) static PY_AWAITABLES_ALLOWED: bool; +} + +fn reject_awaitable_from_sync_caller(result: &Bound<'_, PyAny>) -> FlowResult<()> { + if PY_AWAITABLES_ALLOWED + .try_with(|allowed| *allowed) + .unwrap_or(true) + { + return Ok(()); + } + let _ = result.call_method0("close"); + Err(FlowError::Internal( + "awaitable Python middleware requires an async caller".into(), + )) +} + fn validate_python_llm_sanitizer_signature(py_fn: &Py) -> PyResult<()> { Python::attach(|py| { let inspect = py.import("inspect")?; @@ -79,6 +96,7 @@ fn split_json_or_future( ) -> FlowResult> { let bound = result.bind(py); if bound.getattr("__await__").is_ok() { + reject_awaitable_from_sync_caller(bound)?; let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) .map_err(|e| FlowError::Internal(e.to_string()))?; Ok(Err(Box::pin(future) as PyValueFuture)) @@ -111,6 +129,7 @@ fn split_py_object_or_future( ) -> FlowResult, PyValueFuture>> { let bound = result.bind(py); if bound.getattr("__await__").is_ok() { + reject_awaitable_from_sync_caller(bound)?; let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) .map_err(|e| FlowError::Internal(e.to_string()))?; Ok(Err(Box::pin(future) as PyValueFuture)) @@ -1095,12 +1114,12 @@ pub fn wrap_py_event_subscriber(py_fn: Py) -> EventSubscriberFn { /// Wrap a Python callable ``(Event, EventSanitizeFields) -> EventSanitizeFields``. pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let py_fn = Arc::new(py_fn); - Arc::new(move |event: Event, fields: EventSanitizeFields| { + Arc::new(move |event: Arc, fields: EventSanitizeFields| { let py_fn = py_fn.clone(); Box::pin(async move { let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { - let py_event = match &event { + let py_event = match event.as_ref() { Event::Scope(inner) => Py::new( py, crate::py_types::PyScopeEvent { @@ -1119,27 +1138,18 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let py_event = match py_event { Ok(value) => value, Err(error) => { - eprintln!( - "nemo_relay: failed to convert event sanitizer context: {error}" - ); return Err(FlowError::Internal(error.to_string())); } }; let fields_json = match serde_json::to_value(&fields) { Ok(value) => value, Err(error) => { - eprintln!( - "nemo_relay: failed to serialize event sanitizer fields: {error}" - ); return Err(FlowError::Internal(error.to_string())); } }; let py_fields = match json_to_py(py, &fields_json) { Ok(value) => value, Err(error) => { - eprintln!( - "nemo_relay: failed to convert event sanitizer fields: {error}" - ); return Err(FlowError::Internal(error.to_string())); } }; diff --git a/crates/python/tests/coverage/py_api_coverage_tests.rs b/crates/python/tests/coverage/py_api_coverage_tests.rs index c1cc50293..5c5d35def 100644 --- a/crates/python/tests/coverage/py_api_coverage_tests.rs +++ b/crates/python/tests/coverage/py_api_coverage_tests.rs @@ -185,6 +185,9 @@ def tool_sanitize_response(name, result): def tool_conditional(name, args): return None if args["value"] >= 0 else "blocked" +async def async_tool_conditional(name, args): + return None + def tool_request_intercept(name, args): updated = dict(args) updated["value"] = updated["value"] + 2 @@ -323,6 +326,16 @@ async def run_llm(api, request, func, handle, attributes, codec, response_codec) response_codec=response_codec, ) +async def run_standalone(api, request): + tool_args = await api.tool_request_intercepts("demo-tool", {"value": 1}) + await api.tool_conditional_execution("demo-tool", tool_args) + llm_outcome = await api.llm_request_intercepts("demo-llm", request) + await api.llm_conditional_execution(llm_outcome.request) + return { + "tool_value": tool_args["value"], + "llm_header": llm_outcome.request.headers["x-intercepted"], + } + async def run_stream(api, request, func, collector, finalizer, handle, attributes, codec, response_codec): stream = await api.llm_stream_call_execute( "demo-stream", @@ -498,6 +511,26 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute .to_string() .contains("blocked") ); + let async_sync_rejection_name = format!("async-sync-{}", Uuid::now_v7()); + register_tool_conditional_execution_guardrail( + &async_sync_rejection_name, + 20, + helpers.getattr("async_tool_conditional").unwrap().unbind(), + ) + .unwrap(); + assert!( + tool_conditional_execution( + py, + "demo-tool".to_string(), + &py_dict(py, json!({"value": 1})), + ) + .unwrap_err() + .to_string() + .contains("requires an async caller") + ); + assert!( + deregister_tool_conditional_execution_guardrail(&async_sync_rejection_name).unwrap() + ); let llm_request = PyLLMRequest { inner: nemo_relay::api::llm::LlmRequest { @@ -534,6 +567,21 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute ); with_event_loop(py, |event_loop| { + let standalone = event_loop + .call_method1( + "run_until_complete", + (runner + .getattr("run_standalone") + .unwrap() + .call1((api_module.clone(), llm_request.clone())) + .unwrap(),), + ) + .unwrap(); + assert_eq!( + crate::convert::py_to_json(&standalone).unwrap(), + json!({"tool_value": 3, "llm_header": "1"}) + ); + let tool_result = event_loop .call_method1( "run_until_complete", diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index 89a382f88..53245133c 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -668,7 +668,7 @@ async def collect_stream(awaitable): } #[test] -fn event_sanitize_wrapper_covers_conversion_success_and_fail_closed_paths() { +fn event_sanitize_wrapper_covers_conversion_success_and_error_propagation() { use nemo_relay::api::event::{BaseEvent, MarkEvent}; let _python = crate::test_support::init_python_test(); @@ -704,7 +704,7 @@ def invalid(event, fields): let sanitized = runtime .block_on(wrap_py_event_sanitize_fn( module.getattr("sanitize").unwrap().unbind(), - )(event.clone(), fields.clone())) + )(Arc::new(event.clone()), fields.clone())) .unwrap(); assert_eq!(sanitized.data, Some(json!({"safe": "checkpoint"}))); assert_eq!(sanitized.metadata, None); @@ -712,14 +712,14 @@ def invalid(event, fields): let raised = runtime .block_on(wrap_py_event_sanitize_fn( module.getattr("raises").unwrap().unbind(), - )(event.clone(), fields.clone())) + )(Arc::new(event.clone()), fields.clone())) .unwrap_err(); assert!(raised.to_string().contains("sanitize boom")); let invalid = runtime .block_on(wrap_py_event_sanitize_fn( module.getattr("invalid").unwrap().unbind(), - )(event, fields.clone())) + )(Arc::new(event), fields.clone())) .unwrap_err(); assert!( invalid @@ -728,3 +728,56 @@ def invalid(event, fields): ); }); } + +#[test] +fn awaitable_middleware_wrappers_cover_success_and_failure() { + let _python = crate::test_support::init_python_test(); + Python::attach(|py| { + let module = load_module( + py, + r#" +async def tool_ok(name, args): + return {"name": name, "value": args["value"] + 1} + +async def tool_fail(name, args): + raise RuntimeError("async tool boom") + +async def llm_ok(request): + return None + +async def llm_fail(request): + raise RuntimeError("async llm boom") +"#, + ); + let tool_ok = wrap_py_tool_fn(module.getattr("tool_ok").unwrap().unbind()); + let tool_fail = wrap_py_tool_fn(module.getattr("tool_fail").unwrap().unbind()); + let llm_ok = wrap_py_llm_conditional_fn(module.getattr("llm_ok").unwrap().unbind()); + let llm_fail = wrap_py_llm_conditional_fn(module.getattr("llm_fail").unwrap().unbind()); + + with_event_loop(py, |event_loop| { + pyo3_async_runtimes::tokio::run_until_complete(event_loop, async move { + assert_eq!( + tool_ok("demo".into(), json!({"value": 1})).await.unwrap(), + json!({"name": "demo", "value": 2}) + ); + assert!( + tool_fail("demo".into(), json!({"value": 1})) + .await + .unwrap_err() + .to_string() + .contains("async tool boom") + ); + assert_eq!(llm_ok(make_request()).await.unwrap(), None); + assert!( + llm_fail(make_request()) + .await + .unwrap_err() + .to_string() + .contains("async llm boom") + ); + Ok(()) + }) + .unwrap(); + }); + }); +} diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index dac53db81..9621cf4a1 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -41,6 +41,12 @@ existing error behavior for its middleware family. | Node.js | Direct return value | Direct return value or `Promise` | | Go / raw C FFI | Synchronous callback | Existing synchronous callback, or the new `Async` / completion-based registration API | +Python's standalone middleware helpers preserve their direct synchronous +return when called without a running `asyncio` loop. In that mode, registered +callbacks must also return direct values; an awaitable callback raises a clear +runtime error. Call the helper from async Python and await its result when any +entry may return an awaitable. + For Rust, wrap the existing result in a ready async future, or use an async block when the callback needs to await work: From 33f5149b1572654d829258b42adf85737cc59a95 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 16:06:03 -0400 Subject: [PATCH 17/83] docs: clarify Python awaitable middleware callers Signed-off-by: Will Killian --- docs/about-nemo-relay/concepts/middleware.mdx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/about-nemo-relay/concepts/middleware.mdx b/docs/about-nemo-relay/concepts/middleware.mdx index afd1ae0cf..1af705901 100644 --- a/docs/about-nemo-relay/concepts/middleware.mdx +++ b/docs/about-nemo-relay/concepts/middleware.mdx @@ -22,9 +22,13 @@ hook system. ## Asynchronous Callbacks All middleware families accept asynchronous callbacks. Rust callbacks return a -future; Python callbacks may return a value or an awaitable; and Node callbacks -may return a value or a Promise. Relay awaits entries sequentially in priority -order, so later callbacks observe earlier middleware output. +future, and Node callbacks may return a value or a Promise. Python registrations +accept callbacks that return a value or an awaitable when invoked through an +asynchronous Relay API or queued event publication. Synchronous standalone +Python helpers cannot drive an awaitable callback and raise an error directing +the caller to the corresponding asynchronous helper. Relay awaits entries +sequentially in priority order, so later callbacks observe earlier middleware +output. Managed execution and standalone conditional/request-intercept helpers are asynchronous because their result depends on middleware completion. Manual From 53ba8ffe6070bbc8b7ca98ec301314d9499b6616 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 16:20:25 -0400 Subject: [PATCH 18/83] fix: isolate async event sanitizer panics Signed-off-by: Will Killian --- crates/core/Cargo.toml | 3 +- crates/core/src/api/runtime/state.rs | 21 +++++++++++-- .../src/api/runtime/subscriber_dispatcher.rs | 21 +++---------- .../subscriber_dispatcher_tests.rs | 30 ++++++++++++++----- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 5578e9393..922539111 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -19,7 +19,6 @@ default = [ "object-store", ] atof-streaming = [ - "dep:futures-util", "dep:tokio-tungstenite", "tokio/io-util", "tokio/net", @@ -63,7 +62,7 @@ strum = { version = "0.27", features = ["derive"] } tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "sync", "time"] } tokio-stream = { version = "0.1", default-features = false, features = ["sync"] } typed-builder = "0.23.2" -futures-util = { version = "0.3", optional = true } +futures-util = "0.3" opentelemetry = { workspace = true, features = ["trace"] } opentelemetry-semantic-conventions.workspace = true opentelemetry_sdk = { workspace = true, features = ["trace"] } diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index a70d3a996..44eb7a916 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -10,9 +10,12 @@ use std::any::Any; use std::collections::HashMap; +use std::panic::AssertUnwindSafe; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; +use futures_util::FutureExt; + use crate::api::event::{ BaseEvent, CategoryProfile, Event, EventCategory, MarkEvent, ScopeCategory, ScopeEvent, llm_attributes_to_strings, scope_attributes_to_strings, tool_attributes_to_strings, @@ -640,15 +643,27 @@ impl NemoRelayContextState { let event_context = Arc::new(event.clone()); for entry in entries { let fields = event.sanitize_fields(); - match (entry.payload)(Arc::clone(&event_context), fields).await { - Ok(fields) => event.apply_sanitize_fields(fields), - Err(error) => log::error!( + let callback = Arc::clone(&entry.payload); + let context = Arc::clone(&event_context); + match AssertUnwindSafe(async move { callback(context, fields).await }) + .catch_unwind() + .await + { + Ok(Ok(fields)) => event.apply_sanitize_fields(fields), + Ok(Err(error)) => log::error!( target: "nemo_relay.runtime", event = "event_sanitizer_failed", sanitizer = entry.name.as_str(), event_name = event.name(); "Event sanitizer failed; preserving the last valid event snapshot: {error}" ), + Err(_) => log::error!( + target: "nemo_relay.runtime", + event = "event_sanitizer_panicked", + sanitizer = entry.name.as_str(), + event_name = event.name(); + "Event sanitizer panicked; publishing the latest valid event snapshot" + ), } } event diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 475a0434c..341017a28 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -339,24 +339,11 @@ mod native { if sanitizers.is_empty() { return Some(transformed); } - let original = transformed.clone(); Some( - match catch_unwind(AssertUnwindSafe(|| { - runtime.block_on(NemoRelayContextState::event_sanitize_snapshot_chain( - transformed, - &sanitizers, - )) - })) { - Ok(event) => event, - Err(_) => { - log::error!( - target: "nemo_relay.runtime", - event = "event_sanitizer_panicked"; - "Event sanitizer panicked; publishing the transformed event snapshot" - ); - original - } - }, + runtime.block_on(NemoRelayContextState::event_sanitize_snapshot_chain( + transformed, + &sanitizers, + )), ) } } diff --git a/crates/core/tests/integration/subscriber_dispatcher_tests.rs b/crates/core/tests/integration/subscriber_dispatcher_tests.rs index 303ee112b..07b6429de 100644 --- a/crates/core/tests/integration/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/integration/subscriber_dispatcher_tests.rs @@ -7,6 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, mpsc}; use std::time::Duration; +use nemo_relay::api::event::Event; use nemo_relay::api::registry::{ deregister_mark_sanitize_guardrail, register_mark_sanitize_guardrail, }; @@ -16,6 +17,7 @@ use nemo_relay::api::runtime::{ use nemo_relay::api::scope::{EmitMarkEventParams, event}; use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber}; use nemo_relay::error::FlowError; +use serde_json::json; static TEST_MUTEX: Mutex<()> = Mutex::new(()); @@ -208,15 +210,21 @@ fn dispatcher_publishes_the_snapshot_when_an_async_sanitizer_panics() { reset_global(); setup_isolated_thread(); - let observed = Arc::new(Mutex::new(Vec::new())); + let observed = Arc::new(Mutex::new(Vec::::new())); let observed_events = Arc::clone(&observed); register_subscriber( "panic-sanitizer-subscriber", - Arc::new(move |event| { - observed_events - .lock() - .unwrap() - .push(event.name().to_string()) + Arc::new(move |event| observed_events.lock().unwrap().push(event.clone())), + ) + .unwrap(); + register_mark_sanitize_guardrail( + "successful-mark-sanitizer", + 0, + Arc::new(|_, mut fields| { + Box::pin(async move { + fields.data = Some(json!({"redacted": true})); + Ok(fields) + }) }), ) .unwrap(); @@ -230,7 +238,15 @@ fn dispatcher_publishes_the_snapshot_when_an_async_sanitizer_panics() { emit_mark("panic-fallback"); flush_subscribers().unwrap(); - assert_eq!(observed.lock().unwrap().as_slice(), ["panic-fallback"]); + let events = observed.lock().unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].name(), "panic-fallback"); + assert_eq!( + events[0].sanitize_fields().data, + Some(json!({"redacted": true})) + ); + drop(events); + deregister_mark_sanitize_guardrail("successful-mark-sanitizer").unwrap(); deregister_mark_sanitize_guardrail("panic-mark-sanitizer").unwrap(); deregister_subscriber("panic-sanitizer-subscriber").unwrap(); } From 7217a519a3753d3bc7276dc31ee9933a75f7895c Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 16:50:18 -0400 Subject: [PATCH 19/83] fix: preserve queued sanitizer execution semantics Signed-off-by: Will Killian --- crates/core/src/api/runtime/state.rs | 3 +-- crates/core/src/api/runtime/subscriber_dispatcher.rs | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index 44eb7a916..0e5a2766c 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -640,11 +640,10 @@ impl NemoRelayContextState { mut event: Event, entries: &[Guardrail], ) -> Event { - let event_context = Arc::new(event.clone()); for entry in entries { let fields = event.sanitize_fields(); let callback = Arc::clone(&entry.payload); - let context = Arc::clone(&event_context); + let context = Arc::new(event.clone()); match AssertUnwindSafe(async move { callback(context, fields).await }) .catch_unwind() .await diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 341017a28..102aa0adb 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -121,9 +121,6 @@ mod native { subscribers: &[EventSubscriberFn], scope_stack: ScopeStackHandle, ) -> bool { - if subscribers.is_empty() { - return true; - } let message = DispatcherMessage::Deliver { event: Box::new(event), transform: Some(transform), From d70fbd81ee818ae28b29fed2001e9ecdf62b8198 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 17:09:28 -0400 Subject: [PATCH 20/83] fix: address async middleware review follow-ups Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 207 +++++++------- crates/core/src/api/runtime/state.rs | 254 ++++++++++++++++-- .../src/api/runtime/subscriber_dispatcher.rs | 33 ++- crates/core/src/api/scope.rs | 40 +-- crates/core/src/api/shared.rs | 4 +- crates/core/src/api/tool.rs | 54 ++-- crates/core/src/stream.rs | 42 +-- .../tests/fixtures/native_plugin/src/lib.rs | 6 +- .../tests/integration/middleware_tests.rs | 1 + .../tests/integration/native_plugin_tests.rs | 4 + .../subscriber_dispatcher_tests.rs | 28 +- .../core/tests/unit/dynamic_worker_tests.rs | 127 +++++---- crates/core/tests/unit/llm_api_tests.rs | 19 +- crates/ffi/src/callable.rs | 6 +- crates/ffi/tests/unit/callable_tests.rs | 16 +- crates/node/src/callable.rs | 27 +- crates/node/tests/llm_tests.mjs | 13 +- crates/node/tests/scope_tests.mjs | 13 +- crates/node/tests/test_support.mjs | 24 ++ crates/node/tests/tools_tests.mjs | 18 +- docs/reference/event-sanitizers.mdx | 13 +- docs/reference/migration-guides.mdx | 15 +- 22 files changed, 635 insertions(+), 329 deletions(-) create mode 100644 crates/node/tests/test_support.mjs diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index d0bcc79a3..0301aeeda 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -494,15 +494,16 @@ fn emit_llm_start( let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())? }; - tokio::runtime::Runtime::new() - .map_err(|error| FlowError::Internal(error.to_string()))? - .block_on(emit_llm_start_with_subscribers( + crate::api::runtime::subscriber_dispatcher::block_on_sanitizer_future( + emit_llm_start_with_subscribers( handle, request, annotated_request, request_codec, &subscribers, - )) + ), + ) + .map_err(FlowError::Internal)? } async fn emit_pending_request_marks( @@ -795,6 +796,75 @@ struct LlmCallEndBehavior { attach_estimated_cost: bool, } +struct LlmEndPayload { + data: Option, + annotated_response: Option>, + decode_error: Option, +} + +async fn build_llm_end_payload( + handle: &LlmHandle, + response: Json, + fallback_data: Option, + annotated_response: Option>, + response_codec: Option>, + entries: &[crate::api::registry::Guardrail], + behavior: LlmCallEndBehavior, +) -> LlmEndPayload { + let response_was_null_without_fallback = response.is_null() && fallback_data.is_none(); + let response = if response.is_null() { + fallback_data.unwrap_or(response) + } else { + response + }; + let sanitized_response = NemoRelayContextState::llm_sanitize_response_snapshot_chain( + response.clone(), + LlmSanitizeResponseContext::for_response_codec(response_codec.clone()), + entries, + ) + .await; + let response_changed = sanitized_response + .as_ref() + .is_some_and(|sanitized_response| sanitized_response != &response); + let data = match sanitized_response { + Some(response) if response_was_null_without_fallback && response.is_null() => None, + response => response, + }; + let annotation_omitted = data.as_ref().is_none_or(Json::is_null); + let (mut annotated_response, decode_error) = if annotation_omitted { + (None, None) + } else { + resolve_llm_end_annotation( + (!response_changed).then_some(annotated_response).flatten(), + response_codec, + data.as_ref(), + &behavior, + &handle.name, + ) + }; + let pricing = crate::codec::response::active_pricing_resolver(); + let summary = finalize_optimization_summary( + &handle.optimization_recorder, + annotated_response.as_mut(), + handle.model_name.as_deref(), + &pricing, + ); + if !annotation_omitted + && annotated_response.is_none() + && let Some(summary) = summary + { + annotated_response = Some(AnnotatedLlmResponse { + optimization_summary: Some(summary), + ..AnnotatedLlmResponse::default() + }); + } + LlmEndPayload { + data, + annotated_response: annotated_response.map(Arc::new), + decode_error, + } +} + /// Finish a manual LLM lifecycle span. /// /// This emits an LLM-end event for a handle previously returned by @@ -844,12 +914,8 @@ pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { subscribers, ) }; - let response = if params.response.is_null() { - params.data.unwrap_or(params.response) - } else { - params.response - }; - let response_was_null_without_fallback = response.is_null(); + let response = params.response; + let fallback_data = params.data; let handle = params.handle.clone(); let metadata = params.metadata; let timestamp = params.timestamp; @@ -877,59 +943,26 @@ pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { event, Box::new(move |event| { Box::pin(async move { - let sanitized = NemoRelayContextState::llm_sanitize_response_snapshot_chain( - response.clone(), - LlmSanitizeResponseContext::for_response_codec(response_codec.clone()), + let payload = build_llm_end_payload( + &handle, + response, + fallback_data, + annotated_response, + response_codec, &entries, + LlmCallEndBehavior { + response_codec_errors_fatal: false, + attach_estimated_cost: false, + }, ) .await; - let changed = sanitized - .as_ref() - .is_some_and(|sanitized| sanitized != &response); - let data = match sanitized { - Some(response) if response_was_null_without_fallback && response.is_null() => { - None - } - response => response, - }; - let annotation_omitted = data.as_ref().is_none_or(Json::is_null); - let (mut annotation, decode_error) = if annotation_omitted { - (None, None) - } else { - resolve_llm_end_annotation( - (!changed).then_some(annotated_response).flatten(), - response_codec, - data.as_ref(), - &LlmCallEndBehavior { - response_codec_errors_fatal: false, - attach_estimated_cost: false, - }, - &handle.name, - ) - }; - if let Some(error) = decode_error { + if let Some(error) = payload.decode_error { log::error!( target: "nemo_relay.runtime", event = "manual_llm_response_codec_failed"; "Manual LLM response annotation failed during queued publication: {error}" ); } - let pricing = crate::codec::response::active_pricing_resolver(); - let summary = finalize_optimization_summary( - &handle.optimization_recorder, - annotation.as_mut(), - handle.model_name.as_deref(), - &pricing, - ); - if !annotation_omitted - && annotation.is_none() - && let Some(summary) = summary - { - annotation = Some(AnnotatedLlmResponse { - optimization_summary: Some(summary), - ..AnnotatedLlmResponse::default() - }); - } let context = global_context(); let Ok(state) = context.read() else { return event; @@ -938,9 +971,9 @@ pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { state.build_llm_end_event( EndLlmHandleParams::builder() .handle(&handle) - .data_opt(data) + .data_opt(payload.data) .metadata_opt(end_metadata) - .annotated_response_opt(annotation.map(Arc::new)) + .annotated_response_opt(payload.annotated_response) .timestamp_opt(timestamp) .build(), ) @@ -986,56 +1019,18 @@ async fn llm_call_end_with_behavior( let entries = state.llm_sanitize_response_entries(&scope_locals); (entries, subscribers) }; - let response_was_null_without_fallback = response.is_null() && data.is_none(); - let response = if response.is_null() { - data.unwrap_or(response) - } else { - response - }; - let sanitized_response = NemoRelayContextState::llm_sanitize_response_snapshot_chain( - response.clone(), - LlmSanitizeResponseContext::for_response_codec(response_codec.clone()), + handle.optimization_recorder.close_for_finalization(None); + emit_optimization_marks(handle, &subscribers).await; + let payload = build_llm_end_payload( + handle, + response, + data, + annotated_response, + response_codec, &entries, + behavior, ) .await; - let response_changed = sanitized_response - .as_ref() - .is_some_and(|sanitized_response| sanitized_response != &response); - let data = match sanitized_response { - Some(response) if response_was_null_without_fallback && response.is_null() => None, - response => response, - }; - let annotation_omitted = data.as_ref().is_none_or(Json::is_null); - let (mut annotated_response, decode_error) = if annotation_omitted { - (None, None) - } else { - resolve_llm_end_annotation( - (!response_changed).then_some(annotated_response).flatten(), - response_codec, - data.as_ref(), - &behavior, - &handle.name, - ) - }; - handle.optimization_recorder.close_for_finalization(None); - emit_optimization_marks(handle, &subscribers).await; - let pricing = crate::codec::response::active_pricing_resolver(); - let summary = finalize_optimization_summary( - &handle.optimization_recorder, - annotated_response.as_mut(), - handle.model_name.as_deref(), - &pricing, - ); - if !annotation_omitted - && annotated_response.is_none() - && let Some(summary) = summary - { - annotated_response = Some(AnnotatedLlmResponse { - optimization_summary: Some(summary), - ..AnnotatedLlmResponse::default() - }); - } - let annotated_response = annotated_response.map(Arc::new); let event = { let context = global_context(); let state = context @@ -1045,9 +1040,9 @@ async fn llm_call_end_with_behavior( state.build_llm_end_event( EndLlmHandleParams::builder() .handle(handle) - .data_opt(data) + .data_opt(payload.data) .metadata_opt(end_metadata) - .annotated_response_opt(annotated_response) + .annotated_response_opt(payload.annotated_response) .timestamp_opt(timestamp) .build(), ) @@ -1056,7 +1051,7 @@ async fn llm_call_end_with_behavior( { NemoRelayContextState::emit_event(&event, &subscribers); } - if let Some(error) = decode_error + if let Some(error) = payload.decode_error && behavior.response_codec_errors_fatal { Err(error) diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index 0e5a2766c..b7c6182d4 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -42,6 +42,7 @@ use crate::codec::response::AnnotatedLlmResponse; use crate::context::registries::{ merge_execution_intercept_callables, merge_guardrail_entries, merge_intercept_entries, }; +use crate::error::FlowError; use crate::json::{Json, merge_json}; use crate::registry::SortedRegistry; use chrono::{Duration, Utc}; @@ -703,15 +704,28 @@ impl NemoRelayContextState { ) -> Json { let mut value = args; for entry in entries { - match (entry.payload)(name.to_string(), value.clone()).await { - Ok(next) => value = next, - Err(error) => log::error!( + let callback = Arc::clone(&entry.payload); + let callback_name = name.to_string(); + let current = value.clone(); + match AssertUnwindSafe(async move { callback(callback_name, current).await }) + .catch_unwind() + .await + { + Ok(Ok(next)) => value = next, + Ok(Err(error)) => log::error!( target: "nemo_relay.runtime", event = "tool_request_sanitizer_failed", sanitizer = entry.name.as_str(), tool_name = name; "Tool request sanitizer failed; preserving the last valid payload: {error}" ), + Err(_) => log::error!( + target: "nemo_relay.runtime", + event = "tool_request_sanitizer_panicked", + sanitizer = entry.name.as_str(), + tool_name = name; + "Tool request sanitizer panicked; preserving the last valid payload" + ), } } value @@ -752,15 +766,28 @@ impl NemoRelayContextState { ) -> Json { let mut value = result; for entry in entries { - match (entry.payload)(name.to_string(), value.clone()).await { - Ok(next) => value = next, - Err(error) => log::error!( + let callback = Arc::clone(&entry.payload); + let callback_name = name.to_string(); + let current = value.clone(); + match AssertUnwindSafe(async move { callback(callback_name, current).await }) + .catch_unwind() + .await + { + Ok(Ok(next)) => value = next, + Ok(Err(error)) => log::error!( target: "nemo_relay.runtime", event = "tool_response_sanitizer_failed", sanitizer = entry.name.as_str(), tool_name = name; "Tool response sanitizer failed; preserving the last valid payload: {error}" ), + Err(_) => log::error!( + target: "nemo_relay.runtime", + event = "tool_response_sanitizer_panicked", + sanitizer = entry.name.as_str(), + tool_name = name; + "Tool response sanitizer panicked; preserving the last valid payload" + ), } } value @@ -831,7 +858,20 @@ impl NemoRelayContextState { subscribers, ) .await; - let result = (entry.payload)(name.to_string(), args.clone()).await; + let callback = Arc::clone(&entry.payload); + let callback_name = name.to_string(); + let callback_args = args.clone(); + let result = + match AssertUnwindSafe(async move { callback(callback_name, callback_args).await }) + .catch_unwind() + .await + { + Ok(result) => result, + Err(_) => Err(FlowError::Internal(format!( + "tool conditional guardrail '{}' panicked", + entry.name + ))), + }; let output = match &result { Ok(Some(reason)) => json!({ "allowed": false, @@ -897,7 +937,20 @@ impl NemoRelayContextState { ) -> crate::error::Result { let mut value = args; for entry in entries { - value = (entry.payload.callable)(name.to_string(), value).await?; + let callback = Arc::clone(&entry.payload.callable); + let callback_name = name.to_string(); + value = match AssertUnwindSafe(async move { callback(callback_name, value).await }) + .catch_unwind() + .await + { + Ok(result) => result?, + Err(_) => { + return Err(FlowError::Internal(format!( + "tool request intercept '{}' panicked", + entry.name + ))); + } + }; if entry.payload.break_chain { break; } @@ -1015,9 +1068,17 @@ impl NemoRelayContextState { let mut value = Some(request); for entry in entries { if let Some(current) = value.take() { - match (entry.payload)(current.clone(), context.clone()).await { - Ok(next) => value = next, - Err(error) => { + let callback = Arc::clone(&entry.payload); + let callback_value = current.clone(); + let callback_context = context.clone(); + match AssertUnwindSafe( + async move { callback(callback_value, callback_context).await }, + ) + .catch_unwind() + .await + { + Ok(Ok(next)) => value = next, + Ok(Err(error)) => { log::error!( target: "nemo_relay.runtime", event = "llm_request_sanitizer_failed", @@ -1027,6 +1088,15 @@ impl NemoRelayContextState { ); value = Some(current); } + Err(_) => { + log::error!( + target: "nemo_relay.runtime", + event = "llm_request_sanitizer_panicked", + sanitizer = entry.name.as_str(); + "LLM request sanitizer panicked; preserving the last valid request" + ); + value = Some(current); + } } } } @@ -1068,9 +1138,17 @@ impl NemoRelayContextState { let mut value = Some(response); for entry in entries { if let Some(current) = value.take() { - match (entry.payload)(current.clone(), context.clone()).await { - Ok(next) => value = next, - Err(error) => { + let callback = Arc::clone(&entry.payload); + let callback_value = current.clone(); + let callback_context = context.clone(); + match AssertUnwindSafe( + async move { callback(callback_value, callback_context).await }, + ) + .catch_unwind() + .await + { + Ok(Ok(next)) => value = next, + Ok(Err(error)) => { log::error!( target: "nemo_relay.runtime", event = "llm_response_sanitizer_failed", @@ -1080,6 +1158,15 @@ impl NemoRelayContextState { ); value = Some(current); } + Err(_) => { + log::error!( + target: "nemo_relay.runtime", + event = "llm_response_sanitizer_panicked", + sanitizer = entry.name.as_str(); + "LLM response sanitizer panicked; preserving the last valid response" + ); + value = Some(current); + } } } } @@ -1148,7 +1235,18 @@ impl NemoRelayContextState { subscribers, ) .await; - let result = (entry.payload)(request.clone()).await; + let callback = Arc::clone(&entry.payload); + let callback_request = request.clone(); + let result = match AssertUnwindSafe(async move { callback(callback_request).await }) + .catch_unwind() + .await + { + Ok(result) => result, + Err(_) => Err(FlowError::Internal(format!( + "LLM conditional guardrail '{}' panicked", + entry.name + ))), + }; let output = match &result { Ok(Some(reason)) => json!({ "allowed": false, @@ -1245,8 +1343,22 @@ impl NemoRelayContextState { let mut optimization_contributions = Vec::new(); for entry in entries { let input_content = request_value.content.clone(); - let outcome = - (entry.payload.callable)(name.to_string(), request_value, annotated_value).await?; + let callback = Arc::clone(&entry.payload.callable); + let callback_name = name.to_string(); + let outcome = match AssertUnwindSafe(async move { + callback(callback_name, request_value, annotated_value).await + }) + .catch_unwind() + .await + { + Ok(result) => result?, + Err(_) => { + return Err(FlowError::Internal(format!( + "LLM request intercept '{}' panicked", + entry.name + ))); + } + }; if codec_active && outcome.request.content != input_content { return Err(crate::error::FlowError::InvalidArgument(format!( "LLM request intercept '{}' changed request.content while a request codec is active; modify annotated_request instead", @@ -1354,3 +1466,111 @@ impl Default for NemoRelayContextState { Self::new() } } + +#[cfg(test)] +mod panic_tests { + use super::*; + use crate::api::registry::{RegistryRecord, RequestIntercept}; + use serde_json::{Map, json}; + + #[tokio::test] + async fn middleware_snapshot_chains_contain_callback_panics() { + let tool_payload = json!({"tool": "preserved"}); + let tool_sanitizer: ToolSanitizeFn = + Arc::new(|_, _| Box::pin(async { panic!("tool sanitizer panic") })); + let tool_entries = vec![RegistryRecord::new("tool-panic", 0, tool_sanitizer)]; + assert_eq!( + NemoRelayContextState::tool_sanitize_request_snapshot_chain( + "tool", + tool_payload.clone(), + &tool_entries, + ) + .await, + tool_payload + ); + + let request = LlmRequest { + headers: Map::new(), + content: json!({"llm": "preserved"}), + }; + let llm_sanitizer: LlmSanitizeRequestFn = + Arc::new(|_, _| Box::pin(async { panic!("LLM sanitizer panic") })); + let llm_entries = vec![RegistryRecord::new("llm-panic", 0, llm_sanitizer)]; + assert_eq!( + NemoRelayContextState::llm_sanitize_request_snapshot_chain( + request.clone(), + LlmSanitizeRequestContext::default(), + &llm_entries, + ) + .await, + Some(request.clone()) + ); + + let tool_conditional: ToolConditionalFn = + Arc::new(|_, _| Box::pin(async { panic!("tool conditional panic") })); + let error = NemoRelayContextState::tool_conditional_execution_snapshot_chain( + "tool", + &tool_payload, + &[RegistryRecord::new( + "tool-conditional-panic", + 0, + tool_conditional, + )], + &[], + None, + None, + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("tool-conditional-panic")); + + let llm_conditional: LlmConditionalFn = + Arc::new(|_| Box::pin(async { panic!("LLM conditional panic") })); + let error = NemoRelayContextState::llm_conditional_execution_snapshot_chain( + &request, + &[RegistryRecord::new( + "llm-conditional-panic", + 0, + llm_conditional, + )], + &[], + None, + None, + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("llm-conditional-panic")); + + let tool_intercept: ToolInterceptFn = + Arc::new(|_, _| Box::pin(async { panic!("tool intercept panic") })); + let error = NemoRelayContextState::tool_request_intercepts_snapshot_chain( + "tool", + tool_payload, + &[RegistryRecord::new( + "tool-intercept-panic", + 0, + RequestIntercept::new(false, tool_intercept), + )], + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("tool-intercept-panic")); + + let llm_intercept: LlmRequestInterceptFn = + Arc::new(|_, _, _| Box::pin(async { panic!("LLM intercept panic") })); + let error = NemoRelayContextState::llm_request_intercepts_snapshot_chain( + "llm", + request, + None, + &[RegistryRecord::new( + "llm-intercept-panic", + 0, + RequestIntercept::new(false, llm_intercept), + )], + false, + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("llm-intercept-panic")); + } +} diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 102aa0adb..5421bdca9 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -57,6 +57,25 @@ mod native { static IN_DISPATCHER: Cell = const { Cell::new(false) }; } + fn sanitizer_runtime() -> std::result::Result<&'static tokio::runtime::Runtime, String> { + SANITIZER_RUNTIME + .get_or_init(|| { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| error.to_string()) + }) + .as_ref() + .map_err(Clone::clone) + } + + #[cfg(test)] + pub(super) fn block_on_sanitizer_future( + future: F, + ) -> std::result::Result { + sanitizer_runtime().map(|runtime| runtime.block_on(future)) + } + pub(super) fn dispatch_event(event: &Event, subscribers: &[EventSubscriberFn]) -> bool { if subscribers.is_empty() { return true; @@ -297,12 +316,7 @@ mod native { transform: Option, sanitizers: Vec>, ) -> Option { - let runtime = match SANITIZER_RUNTIME.get_or_init(|| { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| error.to_string()) - }) { + let runtime = match sanitizer_runtime() { Ok(runtime) => runtime, Err(error) => { if !SANITIZER_RUNTIME_FAILURE_LOGGED.swap(true, Ordering::AcqRel) { @@ -345,6 +359,13 @@ mod native { } } +#[cfg(test)] +pub(crate) fn block_on_sanitizer_future( + future: F, +) -> std::result::Result { + native::block_on_sanitizer_future(future) +} + /// Queue an event for subscriber delivery. pub(crate) fn dispatch_event(event: &Event, subscribers: &[EventSubscriberFn]) -> bool { native::dispatch_event(event, subscribers) diff --git a/crates/core/src/api/scope.rs b/crates/core/src/api/scope.rs index 635c6c76c..98afdfbb7 100644 --- a/crates/core/src/api/scope.rs +++ b/crates/core/src/api/scope.rs @@ -52,6 +52,16 @@ pub struct ScopeHandle { pub parent_uuid: Option, } +fn scope_stack_lock_error(error: impl std::fmt::Display, operation: &'static str) -> FlowError { + log::error!( + target: "nemo_relay.runtime", + event = "scope_stack_unavailable", + operation = operation; + "Scope operation failed because the scope stack lock is poisoned: {error}" + ); + FlowError::Internal(error.to_string()) +} + /// Builder parameters for [`push_scope`]. #[derive(TypedBuilder)] #[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))] @@ -224,7 +234,9 @@ pub fn push_scope(params: PushScopeParams<'_>) -> Result { let parent_uuid = resolve_parent_uuid(params.parent); let (handle, event, subscribers, emission_scope_stack) = { let scope_stack = current_scope_stack(); - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_guard = scope_stack + .read() + .map_err(|error| scope_stack_lock_error(error, "push"))?; let scope_subscribers = scope_guard.collect_scope_local_subscribers(); let subscribers = snapshot_event_subscribers(scope_subscribers)?; let context = global_context(); @@ -287,7 +299,9 @@ pub fn pop_scope(params: PopScopeParams<'_>) -> Result<()> { ensure_runtime_owner()?; let scope_stack = current_scope_stack(); let (scope, event, subscribers, emission_scope_stack) = { - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_guard = scope_stack + .read() + .map_err(|error| scope_stack_lock_error(error, "pop"))?; let top = scope_guard.top(); if top.uuid != *params.handle_uuid { if scope_guard.find(params.handle_uuid).is_some() { @@ -361,27 +375,17 @@ pub fn event(params: EmitMarkEventParams<'_>) -> Result<()> { let scope_stack = current_scope_stack(); let (event, subscribers, emission_scope_stack) = { let subscribers = if params.name == COMPACTION_EVENT_NAME { - let mut scope_guard = scope_stack.write().map_err(|error| { - log::error!( - target: "nemo_relay.runtime", - event = "mark_event_scope_stack_unavailable"; - "Mark event was dropped because the scope stack lock is poisoned: {error}" - ); - FlowError::Internal(error.to_string()) - })?; + let mut scope_guard = scope_stack + .write() + .map_err(|error| scope_stack_lock_error(error, "mark"))?; let subscribers = snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?; scope_guard.mark_agent_fresh(parent_uuid); subscribers } else { - let scope_guard = scope_stack.read().map_err(|error| { - log::error!( - target: "nemo_relay.runtime", - event = "mark_event_scope_stack_unavailable"; - "Mark event was dropped because the scope stack lock is poisoned: {error}" - ); - FlowError::Internal(error.to_string()) - })?; + let scope_guard = scope_stack + .read() + .map_err(|error| scope_stack_lock_error(error, "mark"))?; snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())? }; let context = global_context(); diff --git a/crates/core/src/api/shared.rs b/crates/core/src/api/shared.rs index b97c147bb..5491b3182 100644 --- a/crates/core/src/api/shared.rs +++ b/crates/core/src/api/shared.rs @@ -260,7 +260,9 @@ async fn run_request_intercepts_with_codec_inner( let entries = { let scope_stack = current_scope_stack(); - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_guard = scope_stack + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; let scope_locals = scope_guard .collect_scope_local_registries(|registries| ®istries.llm_request_intercepts); diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 0c1128d39..768d31670 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -84,6 +84,25 @@ pub struct CreateToolHandleParams<'a> { pub timestamp: Option>, } +fn resolve_skill_loads( + name: &str, + args: &Json, + metadata: Option<&Json>, +) -> Vec { + let already_handled = metadata + .and_then(Json::as_object) + .and_then(|metadata| metadata.get(skill_load::HANDLED_METADATA_KEY)) + .and_then(Json::as_bool) + .unwrap_or(false); + if already_handled { + Vec::new() + } else if let Some(skill_loads) = skill_load::precomputed(metadata) { + skill_loads + } else { + skill_load::detect(name, args) + } +} + /// Builder parameters for [`NemoRelayContextState::build_tool_end_event`]. #[derive(Debug, Clone, TypedBuilder)] #[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))] @@ -227,20 +246,7 @@ pub fn tool_call(params: ToolCallParams<'_>) -> Result { subscribers, ) }; - let handled_skill_loads = params - .metadata - .as_ref() - .and_then(Json::as_object) - .and_then(|metadata| metadata.get(skill_load::HANDLED_METADATA_KEY)) - .and_then(Json::as_bool) - .is_some_and(|handled| handled); - let skill_loads = if handled_skill_loads { - Vec::new() - } else if let Some(skill_loads) = skill_load::precomputed(params.metadata.as_ref()) { - skill_loads - } else { - skill_load::detect(params.name, ¶ms.args) - }; + let skill_loads = resolve_skill_loads(params.name, ¶ms.args, params.metadata.as_ref()); let raw_args = params.args; let (handle, event, marks) = { let context = global_context(); @@ -301,9 +307,8 @@ pub fn tool_call(params: ToolCallParams<'_>) -> Result { scope_stack.clone(), ); for mark in marks { - if let Some(sanitizers) = snapshot_event_sanitizers(&mark, &scope_stack) { - dispatch_sanitized_event(mark, sanitizers, &subscribers, scope_stack.clone()); - } + let sanitizers = snapshot_event_sanitizers(&mark, &scope_stack).unwrap_or_default(); + dispatch_sanitized_event(mark, sanitizers, &subscribers, scope_stack.clone()); } Ok(handle) } @@ -328,20 +333,7 @@ async fn tool_call_with_subscriber_snapshot( let entries = state.tool_sanitize_request_entries(&scope_locals); (entries, subscribers) }; - let handled_skill_loads = params - .metadata - .as_ref() - .and_then(Json::as_object) - .and_then(|metadata| metadata.get(skill_load::HANDLED_METADATA_KEY)) - .and_then(Json::as_bool) - .is_some_and(|handled| handled); - let skill_loads = if handled_skill_loads { - Vec::new() - } else if let Some(skill_loads) = skill_load::precomputed(params.metadata.as_ref()) { - skill_loads - } else { - skill_load::detect(params.name, ¶ms.args) - }; + let skill_loads = resolve_skill_loads(params.name, ¶ms.args, params.metadata.as_ref()); let sanitized_args = NemoRelayContextState::tool_sanitize_request_snapshot_chain( params.name, params.args, diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index 2e742fea3..2bae33344 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -178,7 +178,7 @@ impl LlmStreamWrapper { &self.scope_stack } - fn finish(&mut self) { + fn finish(&mut self, background_thread: bool) { if self.ended { return; } @@ -193,7 +193,7 @@ impl LlmStreamWrapper { self.handle .optimization_recorder .close_for_finalization(Some("stream_interrupted")); - self.finalization = self.emit_end_event(metadata, true, true); + self.finalization = self.emit_end_event(metadata, true, background_thread); } fn finish_with_status( @@ -236,21 +236,31 @@ impl LlmStreamWrapper { aggregated }; - let snapshot = { - let ss_guard = self.scope_stack.read().expect("scope stack lock poisoned"); - let sl = - ss_guard.collect_scope_local_registries(|r| &r.llm_sanitize_response_guardrails); - let ctx = global_context(); - let state = ctx.read(); - match state { - Ok(state) => { - let entries = state.llm_sanitize_response_entries(&sl); - Some(entries) + let entries = match self.scope_stack.read() { + Ok(scope_guard) => { + let scope_locals = scope_guard + .collect_scope_local_registries(|r| &r.llm_sanitize_response_guardrails); + match global_context().read() { + Ok(state) => state.llm_sanitize_response_entries(&scope_locals), + Err(error) => { + log::error!( + target: "nemo_relay.runtime", + event = "stream_end_sanitizer_snapshot_failed"; + "LLM stream END sanitizer snapshot failed open: {error}" + ); + Vec::new() + } } - Err(_) => None, + } + Err(error) => { + log::error!( + target: "nemo_relay.runtime", + event = "stream_end_sanitizer_snapshot_failed"; + "LLM stream END sanitizer snapshot failed open: {error}" + ); + Vec::new() } }; - let entries = snapshot?; let handle = self.handle.clone(); let scope_stack = self.scope_stack.clone(); let subscribers = self.subscribers.clone(); @@ -468,7 +478,7 @@ impl LlmStreamInner for LlmStreamWrapper { return result.clone(); } let result = this.inner.close().await; - this.finish(); + this.finish(false); if let Some(finalization) = this.finalization.take() { finalization.await.map_err(|error| { FlowError::Internal(format!("stream finalization task failed: {error}")) @@ -741,7 +751,7 @@ fn non_empty_object(object: Map) -> Option { impl Drop for LlmStreamWrapper { fn drop(&mut self) { - self.finish(); + self.finish(true); } } diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index 1367f4d74..d1928528e 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -296,13 +296,13 @@ pub unsafe extern "C" fn nemo_relay_fixture_async_entry( { return NemoRelayStatus::InvalidArg; } - let host_v2 = unsafe { &*(host as *const NemoRelayNativeHostApiV3) }; + let host_v3 = unsafe { &*(host as *const NemoRelayNativeHostApiV3) }; let mut plugin = NemoRelayNativePluginV1::default(); - plugin.plugin_kind = unsafe { raw_host_string(&host_v2.v1, "fixture_async") }; + plugin.plugin_kind = unsafe { raw_host_string(&host_v3.v1, "fixture_async") }; if plugin.plugin_kind.is_null() { return NemoRelayStatus::Internal; } - plugin.user_data = Box::into_raw(Box::new(*host_v2)).cast(); + plugin.user_data = Box::into_raw(Box::new(*host_v3)).cast(); plugin.register = Some(raw_register_async_tool_request); plugin.drop = Some(raw_drop_async_host); unsafe { *out = plugin }; diff --git a/crates/core/tests/integration/middleware_tests.rs b/crates/core/tests/integration/middleware_tests.rs index d448e7867..7e8763850 100644 --- a/crates/core/tests/integration/middleware_tests.rs +++ b/crates/core/tests/integration/middleware_tests.rs @@ -1645,6 +1645,7 @@ async fn test_scope_local_guardrail_lifecycle() { .build(), ) .unwrap(); + flush_subscribers().unwrap(); assert_eq!( call_count.load(Ordering::SeqCst), 1, diff --git a/crates/core/tests/integration/native_plugin_tests.rs b/crates/core/tests/integration/native_plugin_tests.rs index c669f7c46..893c5ca15 100644 --- a/crates/core/tests/integration/native_plugin_tests.rs +++ b/crates/core/tests/integration/native_plugin_tests.rs @@ -673,6 +673,7 @@ async fn native_v3_async_registration_supports_all_middleware_kinds() { manifest_ref: manifest_ref.to_string_lossy().into_owned(), }]) .expect("v3 async native fixture should load"); + let mut cleanup = NativePluginTestCleanup::new(); let mut config = PluginConfig::default(); config.components.push(PluginComponentSpec { kind: "fixture_async".into(), @@ -682,6 +683,7 @@ async fn native_v3_async_registration_supports_all_middleware_kinds() { initialize_plugins_exact(config) .await .expect("v3 async native fixture should register"); + cleanup.mark_plugin_configuration_active(); let rewritten = tool_request_intercepts("async-tool", json!({"input": true})) .await @@ -766,12 +768,14 @@ async fn native_v3_async_registration_supports_all_middleware_kinds() { }); tokio::task::yield_now().await; clear_plugin_configuration().expect("v3 async native fixture should clear while pending"); + cleanup.plugin_configuration_active = false; let pending = pending .await .expect("pending v3 async task should not panic") .expect("pending v3 async request intercept should settle after clear"); assert_eq!(pending["native_async"], true); + drop(cleanup); drop(activation); } diff --git a/crates/core/tests/integration/subscriber_dispatcher_tests.rs b/crates/core/tests/integration/subscriber_dispatcher_tests.rs index 07b6429de..5bdfd7184 100644 --- a/crates/core/tests/integration/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/integration/subscriber_dispatcher_tests.rs @@ -145,12 +145,7 @@ fn dispatcher_publishes_the_snapshot_when_an_async_sanitizer_fails() { let observed_events = Arc::clone(&observed); register_subscriber( "fail-open-sanitizer-subscriber", - Arc::new(move |event| { - observed_events - .lock() - .unwrap() - .push(event.name().to_string()) - }), + Arc::new(move |event| observed_events.lock().unwrap().push(event.clone())), ) .unwrap(); register_mark_sanitize_guardrail( @@ -166,13 +161,28 @@ fn dispatcher_publishes_the_snapshot_when_an_async_sanitizer_fails() { ) .unwrap(); - emit_mark("unsanitized-fallback"); + event( + EmitMarkEventParams::builder() + .name("unsanitized-fallback") + .data(json!({"original_data": true})) + .metadata(json!({"original_metadata": true})) + .build(), + ) + .unwrap(); flush_subscribers().unwrap(); + let observed = observed.lock().unwrap(); + assert_eq!(observed.len(), 1); + assert_eq!(observed[0].name(), "unsanitized-fallback"); + assert_eq!( + observed[0].sanitize_fields().data, + Some(json!({"original_data": true})) + ); assert_eq!( - observed.lock().unwrap().as_slice(), - ["unsanitized-fallback"] + observed[0].sanitize_fields().metadata, + Some(json!({"original_metadata": true})) ); + drop(observed); deregister_mark_sanitize_guardrail("fail-open-mark-sanitizer").unwrap(); deregister_subscriber("fail-open-sanitizer-subscriber").unwrap(); } diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index da3d9da7e..63320c7a8 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -1335,7 +1335,7 @@ async fn install_registrations_covers_registry_error_edges() { } #[tokio::test(flavor = "multi_thread")] -#[allow(clippy::await_holding_lock)] // Serializes access to global runtime state. +#[allow(clippy::await_holding_lock)] // The process-wide test mutex intentionally serializes runtime state. async fn installed_callbacks_apply_surface_specific_fallbacks() { struct RuntimeCleanup { registrations: Option, @@ -1423,64 +1423,79 @@ async fn installed_callbacks_apply_surface_specific_fallbacks() { let llm_request = valid_llm_request(); let llm_response = json!({"response": "preserved"}); - { + let ( + subscribers, + mark_entries, + scope_start_entries, + scope_end_entries, + tool_request_entries, + tool_response_entries, + llm_request_entries, + llm_response_entries, + ) = { let state = context.read().unwrap(); - let subscribers = state.collect_event_subscribers(&[]); - NemoRelayContextState::emit_event(&event, &subscribers); - - for registry in [ - &state.mark_sanitize_guardrails, - &state.scope_sanitize_start_guardrails, - &state.scope_sanitize_end_guardrails, - ] { - let entries = NemoRelayContextState::event_sanitize_entries(registry, &[]); - let sanitized = - NemoRelayContextState::event_sanitize_snapshot_chain(event.clone(), &entries).await; - assert_eq!(sanitized.data(), event.data()); - assert_eq!(sanitized.metadata(), event.metadata()); - } + ( + state.collect_event_subscribers(&[]), + NemoRelayContextState::event_sanitize_entries(&state.mark_sanitize_guardrails, &[]), + NemoRelayContextState::event_sanitize_entries( + &state.scope_sanitize_start_guardrails, + &[], + ), + NemoRelayContextState::event_sanitize_entries( + &state.scope_sanitize_end_guardrails, + &[], + ), + state.tool_sanitize_request_entries(&[]), + state.tool_sanitize_response_entries(&[]), + state.llm_sanitize_request_entries(&[]), + state.llm_sanitize_response_entries(&[]), + ) + }; + NemoRelayContextState::emit_event(&event, &subscribers); - let entries = state.tool_sanitize_request_entries(&[]); - assert_eq!( - NemoRelayContextState::tool_sanitize_request_snapshot_chain( - "tool", - tool_request.clone(), - &entries, - ) - .await, - tool_request - ); - let entries = state.tool_sanitize_response_entries(&[]); - assert_eq!( - NemoRelayContextState::tool_sanitize_response_snapshot_chain( - "tool", - tool_response.clone(), - &entries, - ) - .await, - tool_response - ); - let entries = state.llm_sanitize_request_entries(&[]); - assert_eq!( - NemoRelayContextState::llm_sanitize_request_snapshot_chain( - llm_request.clone(), - crate::api::runtime::LlmSanitizeRequestContext::default(), - &entries, - ) - .await, - Some(llm_request), - ); - let entries = state.llm_sanitize_response_entries(&[]); - assert_eq!( - NemoRelayContextState::llm_sanitize_response_snapshot_chain( - llm_response.clone(), - crate::api::runtime::LlmSanitizeResponseContext::default(), - &entries, - ) - .await, - Some(llm_response), - ); + for entries in [mark_entries, scope_start_entries, scope_end_entries] { + let sanitized = + NemoRelayContextState::event_sanitize_snapshot_chain(event.clone(), &entries).await; + assert_eq!(sanitized.data(), event.data()); + assert_eq!(sanitized.metadata(), event.metadata()); } + + assert_eq!( + NemoRelayContextState::tool_sanitize_request_snapshot_chain( + "tool", + tool_request.clone(), + &tool_request_entries, + ) + .await, + tool_request + ); + assert_eq!( + NemoRelayContextState::tool_sanitize_response_snapshot_chain( + "tool", + tool_response.clone(), + &tool_response_entries, + ) + .await, + tool_response + ); + assert_eq!( + NemoRelayContextState::llm_sanitize_request_snapshot_chain( + llm_request.clone(), + crate::api::runtime::LlmSanitizeRequestContext::default(), + &llm_request_entries, + ) + .await, + Some(llm_request), + ); + assert_eq!( + NemoRelayContextState::llm_sanitize_response_snapshot_chain( + llm_response.clone(), + crate::api::runtime::LlmSanitizeResponseContext::default(), + &llm_response_entries, + ) + .await, + Some(llm_response), + ); crate::api::subscriber::flush_subscribers().expect("subscriber callback should flush"); } diff --git a/crates/core/tests/unit/llm_api_tests.rs b/crates/core/tests/unit/llm_api_tests.rs index 843fe4bf5..8e90747da 100644 --- a/crates/core/tests/unit/llm_api_tests.rs +++ b/crates/core/tests/unit/llm_api_tests.rs @@ -713,10 +713,25 @@ fn buffered_null_fallback_is_sanitized_before_emission() { ) .unwrap(); + let handle = create_llm_handle( + CreateLlmHandleParams::builder() + .name("buffered-explicit-null-fallback") + .build(), + ) + .unwrap(); + llm_call_end( + LlmCallEndParams::builder() + .handle(&handle) + .response(Json::Null) + .data(Json::Null) + .build(), + ) + .unwrap(); + flush_subscribers().unwrap(); let captured = events.lock().unwrap(); assert_eq!(*seen.lock().unwrap(), vec![fallback]); - assert_eq!(captured.len(), 3); + assert_eq!(captured.len(), 4); assert_eq!(captured[0].output(), Some(&Json::Null)); assert!(captured[0].annotated_response().is_none()); assert_eq!(captured[1].output(), Some(&redacted_response())); @@ -726,6 +741,8 @@ fn buffered_null_fallback_is_sanitized_before_emission() { ); assert!(captured[2].output().is_none()); assert!(captured[2].annotated_response().is_none()); + assert_eq!(captured[3].output(), Some(&Json::Null)); + assert!(captured[3].annotated_response().is_none()); assert!( captured .iter() diff --git a/crates/ffi/src/callable.rs b/crates/ffi/src/callable.rs index e876cbe34..0fc04fa9f 100644 --- a/crates/ffi/src/callable.rs +++ b/crates/ffi/src/callable.rs @@ -947,9 +947,11 @@ pub fn wrap_event_sanitize_fn( json_to_c_string(&serde_json::to_value(&fields).unwrap_or(Json::Null)); let result_ptr = unsafe { cb(ud.ptr, &ffi_event, fields_json) }; unsafe { nemo_relay_string_free_internal(fields_json) }; - let result = serde_json::from_value(ptr_to_json(result_ptr)).unwrap_or_default(); + let result = serde_json::from_value(ptr_to_json(result_ptr)).map_err(|error| { + FlowError::Internal(format!("invalid event sanitizer result: {error}")) + }); unsafe { nemo_relay_string_free_internal(result_ptr) }; - Ok(result) + result }) }) } diff --git a/crates/ffi/tests/unit/callable_tests.rs b/crates/ffi/tests/unit/callable_tests.rs index 1ae03a985..70469b4f4 100644 --- a/crates/ffi/tests/unit/callable_tests.rs +++ b/crates/ffi/tests/unit/callable_tests.rs @@ -676,14 +676,18 @@ fn test_wrap_llm_exec_stream_and_event_callbacks() { assert_eq!(sanitize_calls.load(Ordering::SeqCst), 2); let invalid = wrap_event_sanitize_fn(invalid_event_sanitize_cb, std::ptr::null_mut(), None); - assert_eq!( - resolve(invalid(Arc::new(event.clone()), original_fields.clone())).unwrap(), - EventSanitizeFields::default() + assert!( + resolve(invalid(Arc::new(event.clone()), original_fields.clone())) + .unwrap_err() + .to_string() + .contains("invalid event sanitizer result") ); let null = wrap_event_sanitize_fn(null_event_sanitize_cb, std::ptr::null_mut(), None); - assert_eq!( - resolve(null(Arc::new(event), original_fields.clone())).unwrap(), - EventSanitizeFields::default() + assert!( + resolve(null(Arc::new(event), original_fields.clone())) + .unwrap_err() + .to_string() + .contains("invalid event sanitizer result") ); let handle = LlmHandle::builder() diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index c2ef47490..4a7eb2a58 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -705,11 +705,20 @@ pub fn wrap_js_llm_request_intercept_fn( move |name: String, request: LlmRequest, annotated: Option| { let func = func.clone(); Box::pin(async move { - let req_json = serde_json::to_value(&request).unwrap_or(Json::Null); - let annotated_json = annotated - .as_ref() - .map(|a| serde_json::to_value(a).unwrap_or(Json::Null)) - .unwrap_or(Json::Null); + let req_json = serde_json::to_value(&request).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM request intercept request: {error}" + )); + record_callback_error(error.to_string()); + error + })?; + let annotated_json = serde_json::to_value(annotated).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM request intercept annotation: {error}" + )); + record_callback_error(error.to_string()); + error + })?; let arg = serde_json::json!({ "name": name, "request": req_json, @@ -1004,7 +1013,13 @@ pub fn wrap_js_llm_conditional_fn( Arc::new(move |request: LlmRequest| { let func = func.clone(); Box::pin(async move { - let req_json = serde_json::to_value(request).unwrap_or(Json::Null); + let req_json = serde_json::to_value(request).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS LLM conditional request: {error}" + )); + record_callback_error(error.to_string()); + error + })?; let (tx, rx) = tokio::sync::oneshot::channel(); let status = func.call_with_return_value( req_json, diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index 59f63ba33..5408f6fa9 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -8,6 +8,7 @@ import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { waitForSubscriberCallbacks } from './test_support.mjs'; const require = createRequire(import.meta.url); const lib = require('../index.js'); @@ -57,18 +58,6 @@ async function flushSubscriberCallbacks() { } } -async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { - const deadline = Date.now() + timeoutMs; - while (!predicate()) { - await flushSubscribers(); - if (Date.now() >= deadline) { - throw new Error('timed out waiting for subscriber callbacks'); - } - await new Promise((resolve) => setImmediate(resolve)); - } - await flushSubscribers(); -} - function makeNative() { return { headers: {}, diff --git a/crates/node/tests/scope_tests.mjs b/crates/node/tests/scope_tests.mjs index 0332790ac..3a184bfa7 100644 --- a/crates/node/tests/scope_tests.mjs +++ b/crates/node/tests/scope_tests.mjs @@ -4,6 +4,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; +import { waitForSubscriberCallbacks } from './test_support.mjs'; const require = createRequire(import.meta.url); const lib = require('../index.js'); @@ -29,18 +30,6 @@ function rejectWithPrimitive(value) { return Promise.reject(value); } -async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { - const deadline = Date.now() + timeoutMs; - while (!predicate()) { - await flushSubscribers(); - if (Date.now() >= deadline) { - throw new Error('timed out waiting for subscriber callbacks'); - } - await new Promise((resolve) => setImmediate(resolve)); - } - await flushSubscribers(); -} - // =========================================================================== // Scope operations // =========================================================================== diff --git a/crates/node/tests/test_support.mjs b/crates/node/tests/test_support.mjs new file mode 100644 index 000000000..17274dcf0 --- /dev/null +++ b/crates/node/tests/test_support.mjs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const { flushSubscribers } = require('../index.js'); + +export async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { + await flushSubscribers(); + // flushSubscribers() waits for Relay's Rust subscriber dispatcher, but JS + // subscriber callbacks are queued onto Node's event loop through N-API + // ThreadsafeFunction. Yield event-loop turns until the observed JS-side + // callback state is ready, with a timeout to avoid hanging the test forever. + const deadline = Date.now() + timeoutMs; + while (!predicate()) { + await flushSubscribers(); + if (Date.now() >= deadline) { + throw new Error('timed out waiting for subscriber callbacks'); + } + await new Promise((resolve) => setImmediate(resolve)); + } + await flushSubscribers(); +} diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index e104d0611..c9884bc05 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -4,6 +4,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; +import { waitForSubscriberCallbacks } from './test_support.mjs'; const require = createRequire(import.meta.url); const lib = require('../index.js'); @@ -47,23 +48,6 @@ function sparseArray() { return values; } -async function waitForSubscriberCallbacks(predicate, timeoutMs = 15000) { - await flushSubscribers(); - // flushSubscribers() waits for Relay's Rust subscriber dispatcher, but JS - // subscriber callbacks are queued onto Node's event loop through N-API - // ThreadsafeFunction. Yield event-loop turns until the observed JS-side - // callback state is ready, with a timeout to avoid hanging the test forever. - const deadline = Date.now() + timeoutMs; - while (!predicate()) { - await flushSubscribers(); - if (Date.now() >= deadline) { - throw new Error('timed out waiting for subscriber callbacks'); - } - await new Promise((resolve) => setImmediate(resolve)); - } - await flushSubscribers(); -} - // =========================================================================== // Tool lifecycle // =========================================================================== diff --git a/docs/reference/event-sanitizers.mdx b/docs/reference/event-sanitizers.mdx index 6ff89e7e7..6122394b6 100644 --- a/docs/reference/event-sanitizers.mdx +++ b/docs/reference/event-sanitizers.mdx @@ -192,10 +192,15 @@ activation fails. The source-first C API retains `NemoRelayEventSanitizeCb` and adds parallel completion-based async registration APIs. An async callback returns `Complete` -or `Pending` and settles its one-shot completion handle with resolve or reject; -there is no implicit timeout. A callback that returns `Pending` must settle the -handle exactly once, or serial event publication remains blocked. Relay cancels -the handle when the invocation is abandoned; late or duplicate settlement after +or `Pending` and settles its one-shot completion handle with resolve or reject. +The absence of an implicit timeout is intentional: Relay preserves strict FIFO +publication, so one unsettled `Pending` completion blocks every later event in +that publication queue. Plugin authors should arrange their own operation +deadline and settle each retained completion exactly once on every success, +failure, and cancellation path. + +Relay cancels the handle when its invocation is abandoned, which is the +host-supported recovery mechanism; late or duplicate settlement after cancellation is rejected safely. After resolving or rejecting a retained completion, call `nemo_relay_async_completion_release` to release the callback-owned reference. Global names start with `nemo_relay_register_`, and diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index 9621cf4a1..e344cf738 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -20,9 +20,10 @@ observability sanitizer contract. Complete the following migrations before you run existing middleware or a sanitizer with a 0.7 host. -Do not deploy a 0.6 plugin or worker against a 0.7 host. The middleware -callback contract, LLM callback signature, native ABI layout, and worker -invocation schema changed. NeMo Relay does not adapt synchronous Rust +A 0.6 native plugin can still load through the legacy v2 table fallback, but it +is not compatible with the changed middleware and LLM sanitizer callback +contracts or other changed ABI and schema behavior. Rebuild plugins and workers +for 0.7 before using those surfaces. NeMo Relay does not adapt synchronous Rust middleware callbacks or one-argument LLM sanitizer callbacks. @@ -282,9 +283,11 @@ NeMo Relay 0.7 uses native ABI v3. Recompile native plugins against the 0.7 0.7 header. The v3 table preserves the v2 prefix, and Relay retries a legacy v2 table when -loading a plugin that rejects v3. Rebuild anyway if a plugin uses raw ABI -callbacks: v3 adds completion-based async middleware registration, async -execution continuations, and explicit cancellation/late-settlement behavior. +loading a plugin that rejects v3. That fallback supports loading, not +compatibility with changed middleware, LLM sanitizer, ABI, or schema contracts. +Rebuild plugins that use raw ABI callbacks: v3 adds completion-based async +middleware registration, async execution continuations, and explicit +cancellation/late-settlement behavior. The plugin manifest value remains `compat.native_api = "1"`. This manifest contract version is separate from the host ABI version; do not change it to From 85afa47e45fdfb06be2b956eb389470144a598b8 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 17:36:09 -0400 Subject: [PATCH 21/83] test: align sanitizer failure expectations Signed-off-by: Will Killian --- crates/ffi/tests/unit/api/registry_tests.rs | 4 ++-- go/nemo_relay/event_sanitizers_test.go | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/ffi/tests/unit/api/registry_tests.rs b/crates/ffi/tests/unit/api/registry_tests.rs index 7c3af6ee5..f8556a268 100644 --- a/crates/ffi/tests/unit/api/registry_tests.rs +++ b/crates/ffi/tests/unit/api/registry_tests.rs @@ -414,8 +414,8 @@ fn test_ffi_event_sanitizer_registries_and_error_paths() { .iter() .find(|event| event["name"] == "ffi-invalid-callback-mark") .expect("invalid callback mark should be delivered"); - assert_eq!(invalid_callback_event["data"], Json::Null); - assert_eq!(invalid_callback_event["metadata"], Json::Null); + assert_eq!(invalid_callback_event["data"], json!({"secret": true})); + assert_eq!(invalid_callback_event["metadata"], json!({"secret": true})); for name in ["ffi-local-child", "ffi-local-mark"] { for event in events.iter().filter(|event| event["name"] == name) { assert_eq!(event["data"], json!({"sanitized_by": name})); diff --git a/go/nemo_relay/event_sanitizers_test.go b/go/nemo_relay/event_sanitizers_test.go index 5c461fd9a..0a0b91426 100644 --- a/go/nemo_relay/event_sanitizers_test.go +++ b/go/nemo_relay/event_sanitizers_test.go @@ -25,7 +25,7 @@ func TestEventSanitizerRegistries(t *testing.T) { runTestWithScopeStack(t, testEventSanitizerRegistries) } -func TestEventSanitizerMarshalFailureClearsObservabilityFields(t *testing.T) { +func TestEventSanitizerMarshalFailurePreservesObservabilityFields(t *testing.T) { runTestWithScopeStack(t, func(t *testing.T) { var mu sync.Mutex var events []Event @@ -50,8 +50,10 @@ func TestEventSanitizerMarshalFailureClearsObservabilityFields(t *testing.T) { if len(events) != 1 { t.Fatalf("expected one event, got %d", len(events)) } - if len(events[0].Data()) != 0 || len(events[0].CategoryProfile()) != 0 || len(events[0].Metadata()) != 0 { - t.Fatalf("expected cleared observability fields, got data=%s category_profile=%s metadata=%s", events[0].Data(), events[0].CategoryProfile(), events[0].Metadata()) + if string(events[0].Data()) != `{"secret":true}` || + len(events[0].CategoryProfile()) != 0 || + string(events[0].Metadata()) != `{"secret":true}` { + t.Fatalf("expected the last valid observability fields, got data=%s category_profile=%s metadata=%s", events[0].Data(), events[0].CategoryProfile(), events[0].Metadata()) } }) } From 3d7f3667a003a01d0be192c255e4b5281cf53512 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 17:39:24 -0400 Subject: [PATCH 22/83] test: move runtime panic coverage out of source Signed-off-by: Will Killian --- crates/core/src/api/runtime/state.rs | 108 +---------------- crates/core/tests/unit/runtime_state_tests.rs | 110 ++++++++++++++++++ 2 files changed, 112 insertions(+), 106 deletions(-) create mode 100644 crates/core/tests/unit/runtime_state_tests.rs diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index b7c6182d4..1af1314d2 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -1468,109 +1468,5 @@ impl Default for NemoRelayContextState { } #[cfg(test)] -mod panic_tests { - use super::*; - use crate::api::registry::{RegistryRecord, RequestIntercept}; - use serde_json::{Map, json}; - - #[tokio::test] - async fn middleware_snapshot_chains_contain_callback_panics() { - let tool_payload = json!({"tool": "preserved"}); - let tool_sanitizer: ToolSanitizeFn = - Arc::new(|_, _| Box::pin(async { panic!("tool sanitizer panic") })); - let tool_entries = vec![RegistryRecord::new("tool-panic", 0, tool_sanitizer)]; - assert_eq!( - NemoRelayContextState::tool_sanitize_request_snapshot_chain( - "tool", - tool_payload.clone(), - &tool_entries, - ) - .await, - tool_payload - ); - - let request = LlmRequest { - headers: Map::new(), - content: json!({"llm": "preserved"}), - }; - let llm_sanitizer: LlmSanitizeRequestFn = - Arc::new(|_, _| Box::pin(async { panic!("LLM sanitizer panic") })); - let llm_entries = vec![RegistryRecord::new("llm-panic", 0, llm_sanitizer)]; - assert_eq!( - NemoRelayContextState::llm_sanitize_request_snapshot_chain( - request.clone(), - LlmSanitizeRequestContext::default(), - &llm_entries, - ) - .await, - Some(request.clone()) - ); - - let tool_conditional: ToolConditionalFn = - Arc::new(|_, _| Box::pin(async { panic!("tool conditional panic") })); - let error = NemoRelayContextState::tool_conditional_execution_snapshot_chain( - "tool", - &tool_payload, - &[RegistryRecord::new( - "tool-conditional-panic", - 0, - tool_conditional, - )], - &[], - None, - None, - ) - .await - .unwrap_err(); - assert!(error.to_string().contains("tool-conditional-panic")); - - let llm_conditional: LlmConditionalFn = - Arc::new(|_| Box::pin(async { panic!("LLM conditional panic") })); - let error = NemoRelayContextState::llm_conditional_execution_snapshot_chain( - &request, - &[RegistryRecord::new( - "llm-conditional-panic", - 0, - llm_conditional, - )], - &[], - None, - None, - ) - .await - .unwrap_err(); - assert!(error.to_string().contains("llm-conditional-panic")); - - let tool_intercept: ToolInterceptFn = - Arc::new(|_, _| Box::pin(async { panic!("tool intercept panic") })); - let error = NemoRelayContextState::tool_request_intercepts_snapshot_chain( - "tool", - tool_payload, - &[RegistryRecord::new( - "tool-intercept-panic", - 0, - RequestIntercept::new(false, tool_intercept), - )], - ) - .await - .unwrap_err(); - assert!(error.to_string().contains("tool-intercept-panic")); - - let llm_intercept: LlmRequestInterceptFn = - Arc::new(|_, _, _| Box::pin(async { panic!("LLM intercept panic") })); - let error = NemoRelayContextState::llm_request_intercepts_snapshot_chain( - "llm", - request, - None, - &[RegistryRecord::new( - "llm-intercept-panic", - 0, - RequestIntercept::new(false, llm_intercept), - )], - false, - ) - .await - .unwrap_err(); - assert!(error.to_string().contains("llm-intercept-panic")); - } -} +#[path = "../../../tests/unit/runtime_state_tests.rs"] +mod tests; diff --git a/crates/core/tests/unit/runtime_state_tests.rs b/crates/core/tests/unit/runtime_state_tests.rs new file mode 100644 index 000000000..fd6a04f7f --- /dev/null +++ b/crates/core/tests/unit/runtime_state_tests.rs @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Unit tests for runtime middleware snapshot chains. + +use serde_json::{Map, json}; + +use super::*; +use crate::api::registry::{RegistryRecord, RequestIntercept}; + +#[tokio::test] +async fn middleware_snapshot_chains_contain_callback_panics() { + let tool_payload = json!({"tool": "preserved"}); + let tool_sanitizer: ToolSanitizeFn = + Arc::new(|_, _| Box::pin(async { panic!("tool sanitizer panic") })); + let tool_entries = vec![RegistryRecord::new("tool-panic", 0, tool_sanitizer)]; + assert_eq!( + NemoRelayContextState::tool_sanitize_request_snapshot_chain( + "tool", + tool_payload.clone(), + &tool_entries, + ) + .await, + tool_payload + ); + + let request = LlmRequest { + headers: Map::new(), + content: json!({"llm": "preserved"}), + }; + let llm_sanitizer: LlmSanitizeRequestFn = + Arc::new(|_, _| Box::pin(async { panic!("LLM sanitizer panic") })); + let llm_entries = vec![RegistryRecord::new("llm-panic", 0, llm_sanitizer)]; + assert_eq!( + NemoRelayContextState::llm_sanitize_request_snapshot_chain( + request.clone(), + LlmSanitizeRequestContext::default(), + &llm_entries, + ) + .await, + Some(request.clone()) + ); + + let tool_conditional: ToolConditionalFn = + Arc::new(|_, _| Box::pin(async { panic!("tool conditional panic") })); + let error = NemoRelayContextState::tool_conditional_execution_snapshot_chain( + "tool", + &tool_payload, + &[RegistryRecord::new( + "tool-conditional-panic", + 0, + tool_conditional, + )], + &[], + None, + None, + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("tool-conditional-panic")); + + let llm_conditional: LlmConditionalFn = + Arc::new(|_| Box::pin(async { panic!("LLM conditional panic") })); + let error = NemoRelayContextState::llm_conditional_execution_snapshot_chain( + &request, + &[RegistryRecord::new( + "llm-conditional-panic", + 0, + llm_conditional, + )], + &[], + None, + None, + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("llm-conditional-panic")); + + let tool_intercept: ToolInterceptFn = + Arc::new(|_, _| Box::pin(async { panic!("tool intercept panic") })); + let error = NemoRelayContextState::tool_request_intercepts_snapshot_chain( + "tool", + tool_payload, + &[RegistryRecord::new( + "tool-intercept-panic", + 0, + RequestIntercept::new(false, tool_intercept), + )], + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("tool-intercept-panic")); + + let llm_intercept: LlmRequestInterceptFn = + Arc::new(|_, _, _| Box::pin(async { panic!("LLM intercept panic") })); + let error = NemoRelayContextState::llm_request_intercepts_snapshot_chain( + "llm", + request, + None, + &[RegistryRecord::new( + "llm-intercept-panic", + 0, + RequestIntercept::new(false, llm_intercept), + )], + false, + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("llm-intercept-panic")); +} From ca6978b501bedd85c32d0d005ca71f9ff2b3ecd9 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 17:48:26 -0400 Subject: [PATCH 23/83] test: cover all sanitizer panic paths Signed-off-by: Will Killian --- crates/core/tests/unit/runtime_state_tests.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/crates/core/tests/unit/runtime_state_tests.rs b/crates/core/tests/unit/runtime_state_tests.rs index fd6a04f7f..8e6709b91 100644 --- a/crates/core/tests/unit/runtime_state_tests.rs +++ b/crates/core/tests/unit/runtime_state_tests.rs @@ -10,6 +10,25 @@ use crate::api::registry::{RegistryRecord, RequestIntercept}; #[tokio::test] async fn middleware_snapshot_chains_contain_callback_panics() { + let event = Event::Mark(MarkEvent::new( + BaseEvent::builder() + .name("preserved-event") + .data(json!({"event": "preserved"})) + .metadata(json!({"metadata": "preserved"})) + .build(), + None, + None, + )); + let event_sanitizer: EventSanitizeFn = + Arc::new(|_, _| Box::pin(async { panic!("event sanitizer panic") })); + let sanitized_event = NemoRelayContextState::event_sanitize_snapshot_chain( + event.clone(), + &[RegistryRecord::new("event-panic", 0, event_sanitizer)], + ) + .await; + assert_eq!(sanitized_event.data(), event.data()); + assert_eq!(sanitized_event.metadata(), event.metadata()); + let tool_payload = json!({"tool": "preserved"}); let tool_sanitizer: ToolSanitizeFn = Arc::new(|_, _| Box::pin(async { panic!("tool sanitizer panic") })); @@ -23,6 +42,22 @@ async fn middleware_snapshot_chains_contain_callback_panics() { .await, tool_payload ); + let tool_response = json!({"tool_response": "preserved"}); + let tool_response_sanitizer: ToolSanitizeFn = + Arc::new(|_, _| Box::pin(async { panic!("tool response sanitizer panic") })); + assert_eq!( + NemoRelayContextState::tool_sanitize_response_snapshot_chain( + "tool", + tool_response.clone(), + &[RegistryRecord::new( + "tool-response-panic", + 0, + tool_response_sanitizer, + )], + ) + .await, + tool_response + ); let request = LlmRequest { headers: Map::new(), @@ -40,6 +75,22 @@ async fn middleware_snapshot_chains_contain_callback_panics() { .await, Some(request.clone()) ); + let llm_response = json!({"llm_response": "preserved"}); + let llm_response_sanitizer: LlmSanitizeResponseFn = + Arc::new(|_, _| Box::pin(async { panic!("LLM response sanitizer panic") })); + assert_eq!( + NemoRelayContextState::llm_sanitize_response_snapshot_chain( + llm_response.clone(), + LlmSanitizeResponseContext::default(), + &[RegistryRecord::new( + "llm-response-panic", + 0, + llm_response_sanitizer, + )], + ) + .await, + Some(llm_response) + ); let tool_conditional: ToolConditionalFn = Arc::new(|_, _| Box::pin(async { panic!("tool conditional panic") })); From 867f92a5125f5e3dc9eb837492cf41054463ea4d Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 17:56:52 -0400 Subject: [PATCH 24/83] test: assert middleware panic error variants Signed-off-by: Will Killian --- crates/core/tests/unit/runtime_state_tests.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/core/tests/unit/runtime_state_tests.rs b/crates/core/tests/unit/runtime_state_tests.rs index 8e6709b91..09f01f279 100644 --- a/crates/core/tests/unit/runtime_state_tests.rs +++ b/crates/core/tests/unit/runtime_state_tests.rs @@ -108,7 +108,10 @@ async fn middleware_snapshot_chains_contain_callback_panics() { ) .await .unwrap_err(); - assert!(error.to_string().contains("tool-conditional-panic")); + assert!(matches!( + error, + FlowError::Internal(ref message) if message.contains("tool-conditional-panic") + )); let llm_conditional: LlmConditionalFn = Arc::new(|_| Box::pin(async { panic!("LLM conditional panic") })); @@ -125,7 +128,10 @@ async fn middleware_snapshot_chains_contain_callback_panics() { ) .await .unwrap_err(); - assert!(error.to_string().contains("llm-conditional-panic")); + assert!(matches!( + error, + FlowError::Internal(ref message) if message.contains("llm-conditional-panic") + )); let tool_intercept: ToolInterceptFn = Arc::new(|_, _| Box::pin(async { panic!("tool intercept panic") })); @@ -140,7 +146,10 @@ async fn middleware_snapshot_chains_contain_callback_panics() { ) .await .unwrap_err(); - assert!(error.to_string().contains("tool-intercept-panic")); + assert!(matches!( + error, + FlowError::Internal(ref message) if message.contains("tool-intercept-panic") + )); let llm_intercept: LlmRequestInterceptFn = Arc::new(|_, _, _| Box::pin(async { panic!("LLM intercept panic") })); @@ -157,5 +166,8 @@ async fn middleware_snapshot_chains_contain_callback_panics() { ) .await .unwrap_err(); - assert!(error.to_string().contains("llm-intercept-panic")); + assert!(matches!( + error, + FlowError::Internal(ref message) if message.contains("llm-intercept-panic") + )); } From 11c8cae9d69547d6997db0e21353e76aa32993ee Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 18:10:47 -0400 Subject: [PATCH 25/83] fix: address hidden middleware review findings Signed-off-by: Will Killian --- crates/core/src/api/runtime/state.rs | 11 +++--- crates/plugin/tests/typed_callbacks.rs | 50 ++++++++++++++++++++------ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index 1af1314d2..6b36739fa 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -641,10 +641,11 @@ impl NemoRelayContextState { mut event: Event, entries: &[Guardrail], ) -> Event { + let context = Arc::new(event.clone()); for entry in entries { let fields = event.sanitize_fields(); let callback = Arc::clone(&entry.payload); - let context = Arc::new(event.clone()); + let context = Arc::clone(&context); match AssertUnwindSafe(async move { callback(context, fields).await }) .catch_unwind() .await @@ -1083,8 +1084,8 @@ impl NemoRelayContextState { target: "nemo_relay.runtime", event = "llm_request_sanitizer_failed", sanitizer = entry.name.as_str(), - preserved_value = "unsanitized_request"; - "LLM request sanitizer failed; preserving the last valid unsanitized request: {error}" + preserved_value = "last_valid_request"; + "LLM request sanitizer failed; preserving the last valid request: {error}" ); value = Some(current); } @@ -1153,8 +1154,8 @@ impl NemoRelayContextState { target: "nemo_relay.runtime", event = "llm_response_sanitizer_failed", sanitizer = entry.name.as_str(), - preserved_value = "unsanitized_response"; - "LLM response sanitizer failed; preserving the last valid unsanitized response: {error}" + preserved_value = "last_valid_response"; + "LLM response sanitizer failed; preserving the last valid response: {error}" ); value = Some(current); } diff --git a/crates/plugin/tests/typed_callbacks.rs b/crates/plugin/tests/typed_callbacks.rs index 865f05e16..821b8015f 100644 --- a/crates/plugin/tests/typed_callbacks.rs +++ b/crates/plugin/tests/typed_callbacks.rs @@ -18,17 +18,17 @@ use nemo_relay_plugin::{ LlmRequest, LlmRequestInterceptOutcome, LlmStream, LlmStreamNext, NEMO_RELAY_NATIVE_ABI_VERSION, NativePlugin, NemoRelayNativeEventSanitizeCb, NemoRelayNativeEventSubscriberCb, NemoRelayNativeFreeFn, NemoRelayNativeHostApiV1, - NemoRelayNativeLlmCodecKind, NemoRelayNativeLlmConditionalCb, NemoRelayNativeLlmExecutionCb, - NemoRelayNativeLlmRequestCodec, NemoRelayNativeLlmRequestInterceptCb, - NemoRelayNativeLlmResponseCodec, NemoRelayNativeLlmSanitizeRequestCb, - NemoRelayNativeLlmSanitizeRequestContext, NemoRelayNativeLlmSanitizeResponseCb, - NemoRelayNativeLlmSanitizeResponseContext, NemoRelayNativeLlmStreamExecutionCb, - NemoRelayNativeLlmStreamV1, NemoRelayNativePluginContext, NemoRelayNativePluginV1, - NemoRelayNativeScopeHandle, NemoRelayNativeScopeStack, NemoRelayNativeScopeStackBinding, - NemoRelayNativeScopeType, NemoRelayNativeString, NemoRelayNativeToolConditionalCb, - NemoRelayNativeToolExecutionCb, NemoRelayNativeToolJsonCb, NemoRelayNativeWithScopeStackCb, - NemoRelayStatus, PendingMarkSpec, PluginContext, PluginRuntime, ScopeType, - ToolExecutionInterceptOutcome, ToolNext, + NemoRelayNativeHostApiV3, NemoRelayNativeLlmCodecKind, NemoRelayNativeLlmConditionalCb, + NemoRelayNativeLlmExecutionCb, NemoRelayNativeLlmRequestCodec, + NemoRelayNativeLlmRequestInterceptCb, NemoRelayNativeLlmResponseCodec, + NemoRelayNativeLlmSanitizeRequestCb, NemoRelayNativeLlmSanitizeRequestContext, + NemoRelayNativeLlmSanitizeResponseCb, NemoRelayNativeLlmSanitizeResponseContext, + NemoRelayNativeLlmStreamExecutionCb, NemoRelayNativeLlmStreamV1, NemoRelayNativePluginContext, + NemoRelayNativePluginV1, NemoRelayNativeScopeHandle, NemoRelayNativeScopeStack, + NemoRelayNativeScopeStackBinding, NemoRelayNativeScopeType, NemoRelayNativeString, + NemoRelayNativeToolConditionalCb, NemoRelayNativeToolExecutionCb, NemoRelayNativeToolJsonCb, + NemoRelayNativeWithScopeStackCb, NemoRelayStatus, PendingMarkSpec, PluginContext, + PluginRuntime, ScopeType, ToolExecutionInterceptOutcome, ToolNext, }; use serde_json::{Map, json}; @@ -328,6 +328,12 @@ fn native_abi_v3_struct_sizes_are_self_describing() { 280, 288, 296, 304, 312, ] ); + assert_eq!(align_of::(), 8); + assert_eq!(size_of::(), 376); + assert_eq!( + host_api_v3_offsets(), + [0, 320, 328, 336, 344, 352, 360, 368] + ); assert_eq!(align_of::(), 8); assert_eq!(size_of::(), 56); assert_eq!(plugin_offsets(), [0, 8, 16, 24, 32, 40, 48]); @@ -348,6 +354,12 @@ fn native_abi_v3_struct_sizes_are_self_describing() { 152, 156, ] ); + assert_eq!(align_of::(), 4); + assert_eq!(size_of::(), 188); + assert_eq!( + host_api_v3_offsets(), + [0, 160, 164, 168, 172, 176, 180, 184] + ); assert_eq!(align_of::(), 4); assert_eq!(size_of::(), 28); assert_eq!(plugin_offsets(), [0, 4, 8, 12, 16, 20, 24]); @@ -357,6 +369,22 @@ fn native_abi_v3_struct_sizes_are_self_describing() { } } +fn host_api_v3_offsets() -> [usize; 8] { + [ + offset_of!(NemoRelayNativeHostApiV3, v1), + offset_of!(NemoRelayNativeHostApiV3, async_completion_resolve_json), + offset_of!(NemoRelayNativeHostApiV3, async_completion_reject), + offset_of!(NemoRelayNativeHostApiV3, async_completion_is_cancelled), + offset_of!(NemoRelayNativeHostApiV3, async_completion_release), + offset_of!(NemoRelayNativeHostApiV3, async_next_invoke), + offset_of!(NemoRelayNativeHostApiV3, async_next_release), + offset_of!( + NemoRelayNativeHostApiV3, + plugin_context_register_async_middleware + ), + ] +} + fn host_api_offsets() -> [usize; 40] { [ offset_of!(NemoRelayNativeHostApiV1, abi_version), From c623fe1fe07a12fc4f9f0fec79554e98ed2d2507 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 18:23:03 -0400 Subject: [PATCH 26/83] fix: retain Python loop context for sanitizers Signed-off-by: Will Killian --- crates/python/src/py_callable.rs | 35 +++++++++++++++++++++++---- python/tests/test_event_sanitizers.py | 24 ++++++++++++++++++ python/tests/test_llm.py | 33 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 71d423e49..bea95c16d 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -34,6 +34,7 @@ use nemo_relay::api::runtime::{ use nemo_relay::error::{FlowError, Result as FlowResult}; use pyo3::prelude::*; use pyo3::types::PyDict; +use pyo3_async_runtimes::TaskLocals; use serde_json::Value as Json; use tokio_stream::Stream; use tokio_stream::wrappers::ReceiverStream; @@ -126,18 +127,38 @@ async fn resolve_json_or_future( fn split_py_object_or_future( py: Python<'_>, result: Py, +) -> FlowResult, PyValueFuture>> { + split_py_object_or_future_with_locals(py, result, None) +} + +fn split_py_object_or_future_with_locals( + py: Python<'_>, + result: Py, + task_locals: Option<&TaskLocals>, ) -> FlowResult, PyValueFuture>> { let bound = result.bind(py); if bound.getattr("__await__").is_ok() { reject_awaitable_from_sync_caller(bound)?; - let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - Ok(Err(Box::pin(future) as PyValueFuture)) + let future: PyValueFuture = match task_locals { + Some(locals) => Box::pin( + pyo3_async_runtimes::into_future_with_locals(locals, result.into_bound(py)) + .map_err(|e| FlowError::Internal(e.to_string()))?, + ), + None => Box::pin( + pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) + .map_err(|e| FlowError::Internal(e.to_string()))?, + ), + }; + Ok(Err(future)) } else { Ok(Ok(result)) } } +fn capture_python_task_locals() -> Option { + Python::attach(|py| pyo3_async_runtimes::tokio::get_current_locals(py).ok()) +} + async fn resolve_py_object_or_future( outcome: FlowResult, PyValueFuture>>, ) -> FlowResult> { @@ -1048,8 +1069,10 @@ pub fn wrap_py_finalizer_fn(py_fn: Py) -> Box Json + Send /// Wrap a Python callable `(Json, LlmSanitizeResponseContext) -> Optional[Json]`. fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeResponseFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { let py_fn = py_fn.clone(); + let task_locals = task_locals.clone(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let py_context = PyLlmSanitizeResponseContext { inner: context }; @@ -1058,7 +1081,7 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon let result = py_fn .call1(py, (py_response, py_context)) .map_err(|error| FlowError::Internal(error.to_string()))?; - split_py_object_or_future(py, result) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) })) .await?; Python::attach(|py| { @@ -1114,8 +1137,10 @@ pub fn wrap_py_event_subscriber(py_fn: Py) -> EventSubscriberFn { /// Wrap a Python callable ``(Event, EventSanitizeFields) -> EventSanitizeFields``. pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new(move |event: Arc, fields: EventSanitizeFields| { let py_fn = py_fn.clone(); + let task_locals = task_locals.clone(); Box::pin(async move { let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { @@ -1156,7 +1181,7 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let result = py_fn .call1(py, (py_event, py_fields)) .map_err(|error| FlowError::Internal(error.to_string()))?; - split_py_object_or_future(py, result) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) }, ); let result = resolve_py_object_or_future(result).await?; diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 08234b70e..4a91bf658 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -3,6 +3,7 @@ from __future__ import annotations +import asyncio from collections.abc import Iterator from typing import cast @@ -73,6 +74,29 @@ def raises(_event: nemo_relay.Event, _fields: EventSanitizeFields) -> EventSanit assert events[-1].metadata is None +async def test_async_mark_sanitizer_runs_on_originating_loop(capture_events): + _capture_name, events = capture_events + originating_loop = asyncio.get_running_loop() + + async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + await asyncio.sleep(0) + assert asyncio.get_running_loop() is originating_loop + return { + "data": {"async": True}, + "category_profile": fields["category_profile"], + "metadata": fields["metadata"], + } + + guardrails.register_mark_sanitize("python-async-mark", 0, sanitize) + try: + scope.event("async-checkpoint", data={"raw": True}) + await asyncio.to_thread(subscribers.flush) + finally: + guardrails.deregister_mark_sanitize("python-async-mark") + + assert events[-1].data == {"async": True} + + def test_scope_start_and_end_sanitizers_cover_category_profile(capture_events): _capture_name, events = capture_events diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index 411629401..3da6c3d68 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -665,6 +665,39 @@ def finalizer(): # Collector should have received all chunks assert len(collected) == len(chunks) + async def test_async_response_sanitizer_runs_during_stream_finalization(self): + events = [] + originating_loop = asyncio.get_running_loop() + subscribers.register("py_llm_async_stream_sanitizer_sub", events.append) + + async def sanitize_response(response, context): + del context + await asyncio.sleep(0) + assert asyncio.get_running_loop() is originating_loop + return {"sanitized": response["raw"]} + + async def stream_func(request): + del request + yield {"token": "hello"} + + guardrails.register_llm_sanitize_response("py_llm_async_stream_sanitizer", 1, sanitize_response) + try: + stream = await llm.stream_execute( + "stream_async_response_sanitizer", + make_request(), + stream_func, + lambda chunk: None, + lambda: {"raw": True}, + ) + assert [chunk async for chunk in stream] == [{"token": "hello"}] + await asyncio.to_thread(subscribers.flush) + finally: + guardrails.deregister_llm_sanitize_response("py_llm_async_stream_sanitizer") + subscribers.deregister("py_llm_async_stream_sanitizer_sub") + + end = _llm_event(events, "stream_async_response_sanitizer", "end") + assert end.data == {"sanitized": True} + async def test_stream_execute_aclose_stops_partially_consumed_stream(self): producer_closed = asyncio.Event() wait_for_more_chunks = asyncio.Event() From c073e9cdc0c3b9f31c23dbad9a7ad69ae0f8bab0 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 18:41:07 -0400 Subject: [PATCH 27/83] fix: preserve progressive event sanitizer context Signed-off-by: Will Killian --- crates/core/src/api/runtime/state.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index 6b36739fa..b83e3d170 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -641,15 +641,16 @@ impl NemoRelayContextState { mut event: Event, entries: &[Guardrail], ) -> Event { - let context = Arc::new(event.clone()); for entry in entries { let fields = event.sanitize_fields(); let callback = Arc::clone(&entry.payload); - let context = Arc::clone(&context); - match AssertUnwindSafe(async move { callback(context, fields).await }) + let context = Arc::new(event); + let callback_context = Arc::clone(&context); + let outcome = AssertUnwindSafe(async move { callback(callback_context, fields).await }) .catch_unwind() - .await - { + .await; + event = Arc::try_unwrap(context).unwrap_or_else(|context| (*context).clone()); + match outcome { Ok(Ok(fields)) => event.apply_sanitize_fields(fields), Ok(Err(error)) => log::error!( target: "nemo_relay.runtime", From 3fc90f85cf472db1f06840db736bdcfc9cc7c182 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 19:12:55 -0400 Subject: [PATCH 28/83] fix: address async middleware review findings Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 17 ++++- crates/core/src/api/tool.rs | 24 ++---- crates/core/src/plugin/dynamic/native.rs | 25 ++++++- crates/core/src/stream.rs | 6 +- .../tests/fixtures/native_plugin/src/lib.rs | 49 +++++++----- .../tests/integration/native_plugin_tests.rs | 17 ++++- crates/plugin/src/lib.rs | 67 +++++++++++++---- crates/plugin/tests/typed_callbacks.rs | 17 ++++- crates/python/src/py_api/mod.rs | 74 +++++++++++-------- python/tests/test_llm.py | 7 +- 10 files changed, 211 insertions(+), 92 deletions(-) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 5421bdca9..1d46f3fe6 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -350,12 +350,23 @@ mod native { if sanitizers.is_empty() { return Some(transformed); } - Some( + let fallback = transformed.clone(); + match catch_unwind(AssertUnwindSafe(|| { runtime.block_on(NemoRelayContextState::event_sanitize_snapshot_chain( transformed, &sanitizers, - )), - ) + )) + })) { + Ok(event) => Some(event), + Err(_) => { + log::error!( + target: "nemo_relay.runtime", + event = "event_sanitizer_panicked"; + "Event sanitizer panicked; preserving the last valid event snapshot" + ); + Some(fallback) + } + } } } diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 768d31670..671471748 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -377,17 +377,13 @@ async fn tool_call_with_subscriber_snapshot( .collect::>(); (handle, event, marks) }; - let mut sanitized_marks = Vec::with_capacity(marks.len()); - for mark in marks { - if let Some(mark) = sanitize_event(mark).await { - sanitized_marks.push(mark); - } - } if let Some(event) = sanitize_event(event).await { NemoRelayContextState::emit_event(&event, &subscribers); } - for mark in sanitized_marks { - NemoRelayContextState::emit_event(&mark, &subscribers); + for mark in marks { + if let Some(mark) = sanitize_event(mark).await { + NemoRelayContextState::emit_event(&mark, &subscribers); + } } Ok((handle, subscribers)) } @@ -549,17 +545,13 @@ async fn tool_call_end_with_pending_marks( )) }) .collect::>(); - let mut sanitized_marks = Vec::with_capacity(marks.len()); - for mark in marks { - if let Some(mark) = sanitize_event(mark).await { - sanitized_marks.push(mark); - } - } if let Some(event) = sanitize_event(event).await { NemoRelayContextState::emit_event(&event, subscribers); } - for mark in sanitized_marks { - NemoRelayContextState::emit_event(&mark, subscribers); + for mark in marks { + if let Some(mark) = sanitize_event(mark).await { + NemoRelayContextState::emit_event(&mark, subscribers); + } } Ok(()) } diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 084a8900b..0f51a25c4 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -1465,6 +1465,22 @@ async fn invoke_native_async_callback( } }; unsafe { native_string_free(invocation as *mut NemoRelayNativeString) }; + let state = match NemoRelayNativeAsyncCallbackState::try_from(state) { + Ok(state) => state, + Err(()) => { + unsafe { + drop(Arc::from_raw( + completion_ref as *const NativeAsyncCompletion, + )); + if let Some(next_ref) = next_ref { + drop(Arc::from_raw(next_ref as *const NativeAsyncNext)); + } + } + return Err(FlowError::Internal( + "native async callback returned an invalid state".into(), + )); + } + }; if state == NemoRelayNativeAsyncCallbackState::Complete { unsafe { drop(Arc::from_raw( @@ -1935,7 +1951,7 @@ fn wrap_native_async_llm_stream_execution( unsafe extern "C" fn native_plugin_context_register_async_middleware( ctx: *mut NemoRelayNativePluginContext, - kind: NemoRelayNativeAsyncMiddlewareKind, + kind: u32, name: *const NemoRelayNativeString, priority: i32, break_chain: bool, @@ -1953,6 +1969,13 @@ unsafe extern "C" fn native_plugin_context_register_async_middleware( Ok(name) => name, Err(status) => return status, }; + let kind = match NemoRelayNativeAsyncMiddlewareKind::try_from(kind) { + Ok(kind) => kind, + Err(()) => { + set_native_last_error("invalid native async middleware kind"); + return NemoRelayStatus::InvalidArg; + } + }; let context = unsafe { &mut *host_ctx.ctx }; let registration = match kind { NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest => context diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index 2bae33344..a7cd81761 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -389,9 +389,9 @@ impl LlmStreamWrapper { Err(_) => None, } }; - if let Some(event) = event_snapshot - && let Some(sanitizers) = snapshot_event_sanitizers(&event, &self.scope_stack) - { + if let Some(event) = event_snapshot { + let sanitizers = + snapshot_event_sanitizers(&event, &self.scope_stack).unwrap_or_default(); let _ = subscriber_dispatcher::dispatch_sanitized_event( event, sanitizers, diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index d1928528e..00d417117 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -3,6 +3,7 @@ use std::ffi::c_void; use std::ptr; +use std::sync::atomic::{AtomicBool, Ordering}; use nemo_relay_plugin::{ CategoryProfile, ConfigDiagnostic, DiagnosticLevel, Event, EventCategory, EventSanitizeFields, @@ -17,6 +18,13 @@ use serde_json::{Map, json}; struct FixtureNativePlugin; +static ASYNC_PENDING_ENTERED: AtomicBool = AtomicBool::new(false); + +#[unsafe(no_mangle)] +pub extern "C" fn nemo_relay_fixture_async_pending_entered() -> bool { + ASYNC_PENDING_ENTERED.swap(false, Ordering::AcqRel) +} + impl NativePlugin for FixtureNativePlugin { fn plugin_kind(&self) -> &str { "fixture_native" @@ -634,7 +642,7 @@ unsafe extern "C" fn raw_register_async_tool_request( } let status = unsafe { (host.plugin_context_register_async_middleware)( - ctx, kind, name, 0, false, callback, user_data, None, + ctx, kind as u32, name, 0, false, callback, user_data, None, ) }; unsafe { (host.v1.string_free)(name) }; @@ -650,9 +658,9 @@ unsafe extern "C" fn raw_async_allow_callback( _invocation_json: *const NemoRelayNativeString, _next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, -) -> NemoRelayNativeAsyncCallbackState { +) -> u32 { let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; }; let result = unsafe { raw_host_string(&host.v1, "null") }; if result.is_null() { @@ -663,7 +671,7 @@ unsafe extern "C" fn raw_async_allow_callback( (host.v1.string_free)(result); } } - NemoRelayNativeAsyncCallbackState::Complete + NemoRelayNativeAsyncCallbackState::Complete as u32 } unsafe extern "C" fn raw_async_passthrough_callback( @@ -671,9 +679,9 @@ unsafe extern "C" fn raw_async_passthrough_callback( invocation_json: *const NemoRelayNativeString, _next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, -) -> NemoRelayNativeAsyncCallbackState { +) -> u32 { let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; }; let result = unsafe { raw_host_string_value(&host.v1, invocation_json) } .and_then(|value| serde_json::from_str::(&value).ok()) @@ -694,7 +702,7 @@ unsafe extern "C" fn raw_async_passthrough_callback( .and_then(|value| serde_json::to_string(&value).ok()); let Some(result) = result else { unsafe { reject_async_completion(host, completion, "invalid async passthrough invocation") }; - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; }; let result = unsafe { raw_host_string(&host.v1, &result) }; if result.is_null() { @@ -705,7 +713,7 @@ unsafe extern "C" fn raw_async_passthrough_callback( (host.v1.string_free)(result); } } - NemoRelayNativeAsyncCallbackState::Complete + NemoRelayNativeAsyncCallbackState::Complete as u32 } unsafe extern "C" fn raw_async_tool_request_callback( @@ -713,9 +721,9 @@ unsafe extern "C" fn raw_async_tool_request_callback( invocation_json: *const NemoRelayNativeString, _next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, -) -> NemoRelayNativeAsyncCallbackState { +) -> u32 { let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; }; let invocation = unsafe { raw_host_string_value(&host.v1, invocation_json) } .and_then(|json| serde_json::from_str::(&json).ok()) @@ -737,9 +745,10 @@ unsafe extern "C" fn raw_async_tool_request_callback( }); let Some((result, pending, duplicate)) = invocation else { unsafe { reject_async_completion(host, completion, "invalid async tool request invocation") }; - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; }; if pending { + ASYNC_PENDING_ENTERED.store(true, Ordering::Release); let host = *host; let completion = completion as usize; std::thread::spawn(move || { @@ -769,7 +778,7 @@ unsafe extern "C" fn raw_async_tool_request_callback( } } }); - return NemoRelayNativeAsyncCallbackState::Pending; + return NemoRelayNativeAsyncCallbackState::Pending as u32; } let result = unsafe { raw_host_string(&host.v1, &result) }; if !result.is_null() { @@ -783,7 +792,7 @@ unsafe extern "C" fn raw_async_tool_request_callback( } else { unsafe { reject_async_completion(host, completion, "failed to allocate async tool request result") }; } - NemoRelayNativeAsyncCallbackState::Complete + NemoRelayNativeAsyncCallbackState::Complete as u32 } unsafe extern "C" fn raw_async_tool_execution_callback( @@ -791,16 +800,16 @@ unsafe extern "C" fn raw_async_tool_execution_callback( invocation_json: *const NemoRelayNativeString, next: *const nemo_relay_plugin::NemoRelayNativeAsyncNext, completion: *const nemo_relay_plugin::NemoRelayNativeAsyncCompletion, -) -> NemoRelayNativeAsyncCallbackState { +) -> u32 { let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; }; if next.is_null() || completion.is_null() { unsafe { reject_async_completion(host, completion, "async tool execution requires next and completion") }; if !next.is_null() { unsafe { (host.async_next_release)(next) }; } - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; } let value = unsafe { raw_host_string_value(&host.v1, invocation_json) } .and_then(|json| serde_json::from_str::(&json).ok()) @@ -816,13 +825,13 @@ unsafe extern "C" fn raw_async_tool_execution_callback( let Some(value) = value else { unsafe { reject_async_completion(host, completion, "invalid async tool execution invocation") }; unsafe { (host.async_next_release)(next) }; - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; }; let value = unsafe { raw_host_string(&host.v1, &value) }; if value.is_null() { unsafe { reject_async_completion(host, completion, "failed to allocate async tool execution invocation") }; unsafe { (host.async_next_release)(next) }; - return NemoRelayNativeAsyncCallbackState::Complete; + return NemoRelayNativeAsyncCallbackState::Complete as u32; } let status = unsafe { (host.async_next_invoke)(next, value, completion) }; unsafe { @@ -833,11 +842,11 @@ unsafe extern "C" fn raw_async_tool_execution_callback( (host.async_next_release)(next); (host.async_completion_release)(completion); } - NemoRelayNativeAsyncCallbackState::Pending + NemoRelayNativeAsyncCallbackState::Pending as u32 } else { unsafe { reject_async_completion(host, completion, "failed to invoke async tool execution next") }; unsafe { (host.async_next_release)(next) }; - NemoRelayNativeAsyncCallbackState::Complete + NemoRelayNativeAsyncCallbackState::Complete as u32 } } diff --git a/crates/core/tests/integration/native_plugin_tests.rs b/crates/core/tests/integration/native_plugin_tests.rs index 893c5ca15..91a07a4cf 100644 --- a/crates/core/tests/integration/native_plugin_tests.rs +++ b/crates/core/tests/integration/native_plugin_tests.rs @@ -673,6 +673,15 @@ async fn native_v3_async_registration_supports_all_middleware_kinds() { manifest_ref: manifest_ref.to_string_lossy().into_owned(), }]) .expect("v3 async native fixture should load"); + let fixture_library = unsafe { libloading::Library::new(&fixture.library_path) } + .expect("loaded v3 async native fixture should open for synchronization"); + let pending_entered = unsafe { + *fixture_library + .get:: bool>(b"nemo_relay_fixture_async_pending_entered\0") + .expect("v3 async native fixture should export its pending-entry signal") + }; + assert!(!unsafe { pending_entered() }); + drop(fixture_library); let mut cleanup = NativePluginTestCleanup::new(); let mut config = PluginConfig::default(); config.components.push(PluginComponentSpec { @@ -766,7 +775,13 @@ async fn native_v3_async_registration_supports_all_middleware_kinds() { let pending = tokio::spawn(async { tool_request_intercepts("async-pending", json!({"input": true})).await }); - tokio::task::yield_now().await; + tokio::time::timeout(std::time::Duration::from_secs(10), async { + while !unsafe { pending_entered() } { + tokio::task::yield_now().await; + } + }) + .await + .expect("native async callback should enter before plugin clear"); clear_plugin_configuration().expect("v3 async native fixture should clear while pending"); cleanup.plugin_configuration_active = false; let pending = pending diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 5f5500072..1e08c05f7 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -800,6 +800,30 @@ pub enum NemoRelayNativeAsyncMiddlewareKind { ScopeSanitizeEnd = 13, } +impl TryFrom for NemoRelayNativeAsyncMiddlewareKind { + type Error = (); + + fn try_from(value: u32) -> std::result::Result { + match value { + 0 => Ok(Self::ToolSanitizeRequest), + 1 => Ok(Self::ToolSanitizeResponse), + 2 => Ok(Self::ToolConditionalExecution), + 3 => Ok(Self::ToolRequestIntercept), + 4 => Ok(Self::ToolExecutionIntercept), + 5 => Ok(Self::LlmSanitizeRequest), + 6 => Ok(Self::LlmSanitizeResponse), + 7 => Ok(Self::LlmConditionalExecution), + 8 => Ok(Self::LlmRequestIntercept), + 9 => Ok(Self::LlmExecutionIntercept), + 10 => Ok(Self::LlmStreamExecutionIntercept), + 11 => Ok(Self::MarkSanitize), + 12 => Ok(Self::ScopeSanitizeStart), + 13 => Ok(Self::ScopeSanitizeEnd), + _ => Err(()), + } + } +} + /// Indicates whether an asynchronous native callback settled before returning. #[repr(u32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -810,6 +834,18 @@ pub enum NemoRelayNativeAsyncCallbackState { Pending = 1, } +impl TryFrom for NemoRelayNativeAsyncCallbackState { + type Error = (); + + fn try_from(value: u32) -> std::result::Result { + match value { + 0 => Ok(Self::Complete), + 1 => Ok(Self::Pending), + _ => Err(()), + } + } +} + /// Opaque one-shot completion retained by a pending native callback. #[repr(C)] pub struct NemoRelayNativeAsyncCompletion { @@ -827,18 +863,18 @@ pub struct NemoRelayNativeAsyncNext { /// Completion-based native middleware callback. /// /// `invocation_json` is borrowed for the call. A callback that returns -/// [`NemoRelayNativeAsyncCallbackState::Pending`] owns one completion -/// reference and must settle it then call the v3 `async_completion_release` -/// hook. When `next` is non-null, the callback owns that handle for the -/// invocation and must call `async_next_release` after its final use. `next` -/// is null for non-execution middleware. -pub type NemoRelayNativeAsyncMiddlewareCb = - unsafe extern "C" fn( - user_data: *mut c_void, - invocation_json: *const NemoRelayNativeString, - next: *const NemoRelayNativeAsyncNext, - completion: *const NemoRelayNativeAsyncCompletion, - ) -> NemoRelayNativeAsyncCallbackState; +/// [`NemoRelayNativeAsyncCallbackState::Pending`] as a `u32` owns one +/// completion reference and must settle it then call the v3 +/// `async_completion_release` hook. The host validates the returned +/// discriminant. When `next` is non-null, the callback owns that handle for +/// the invocation and must call `async_next_release` after its final use. +/// `next` is null for non-execution middleware. +pub type NemoRelayNativeAsyncMiddlewareCb = unsafe extern "C" fn( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + next: *const NemoRelayNativeAsyncNext, + completion: *const NemoRelayNativeAsyncCompletion, +) -> u32; /// ABI-v3 host extension appended to [`NemoRelayNativeHostApiV1`]. /// @@ -874,9 +910,12 @@ pub struct NemoRelayNativeHostApiV3 { /// Releases the callback-owned continuation reference for a pending callback. pub async_next_release: unsafe extern "C" fn(next: *const NemoRelayNativeAsyncNext), /// Registers any completion-based asynchronous middleware surface. + /// + /// `kind` must be a valid [`NemoRelayNativeAsyncMiddlewareKind`] + /// discriminant. The host rejects unknown `u32` values. pub plugin_context_register_async_middleware: unsafe extern "C" fn( ctx: *mut NemoRelayNativePluginContext, - kind: NemoRelayNativeAsyncMiddlewareKind, + kind: u32, name: *const NemoRelayNativeString, priority: i32, break_chain: bool, @@ -2396,7 +2435,7 @@ impl<'a> PluginContext<'a> { self.with_name(name, |_, name| unsafe { (host.plugin_context_register_async_middleware)( self.raw, - kind, + kind as u32, name, priority, break_chain, diff --git a/crates/plugin/tests/typed_callbacks.rs b/crates/plugin/tests/typed_callbacks.rs index 821b8015f..eccca24aa 100644 --- a/crates/plugin/tests/typed_callbacks.rs +++ b/crates/plugin/tests/typed_callbacks.rs @@ -16,7 +16,8 @@ use nemo_relay_plugin::{ AnnotatedLlmRequest, BuiltinLlmCodec, CategoryProfile, ConfigDiagnostic, DiagnosticLevel, Event, EventCategory, EventSanitizeFields, Json, LlmCodecIdentity, LlmJsonStream, LlmNext, LlmRequest, LlmRequestInterceptOutcome, LlmStream, LlmStreamNext, - NEMO_RELAY_NATIVE_ABI_VERSION, NativePlugin, NemoRelayNativeEventSanitizeCb, + NEMO_RELAY_NATIVE_ABI_VERSION, NativePlugin, NemoRelayNativeAsyncCallbackState, + NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativeEventSanitizeCb, NemoRelayNativeEventSubscriberCb, NemoRelayNativeFreeFn, NemoRelayNativeHostApiV1, NemoRelayNativeHostApiV3, NemoRelayNativeLlmCodecKind, NemoRelayNativeLlmConditionalCb, NemoRelayNativeLlmExecutionCb, NemoRelayNativeLlmRequestCodec, @@ -32,6 +33,20 @@ use nemo_relay_plugin::{ }; use serde_json::{Map, json}; +#[test] +fn async_abi_discriminants_reject_unknown_values() { + assert_eq!( + NemoRelayNativeAsyncMiddlewareKind::try_from(13), + Ok(NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd) + ); + assert!(NemoRelayNativeAsyncMiddlewareKind::try_from(14).is_err()); + assert_eq!( + NemoRelayNativeAsyncCallbackState::try_from(1), + Ok(NemoRelayNativeAsyncCallbackState::Pending) + ); + assert!(NemoRelayNativeAsyncCallbackState::try_from(2).is_err()); +} + struct TestString(Vec); struct RegisteredSubscriber { diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index bf6280c04..534f68c99 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1327,13 +1327,17 @@ fn tool_request_intercepts<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - let result = pyo3_async_runtimes::tokio::get_runtime() - .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_tool_api::tool_request_intercepts(&name, args_json).await - }), - )) + let result = py + .detach(|| { + pyo3_async_runtimes::tokio::get_runtime().block_on( + py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_tool_api::tool_request_intercepts(&name, args_json).await + }), + ), + ) + }) .map_err(to_py_err)?; return json_to_py(py, &result).map(|value| value.into_bound(py)); } @@ -1370,14 +1374,17 @@ fn tool_conditional_execution<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - pyo3_async_runtimes::tokio::get_runtime() - .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_tool_api::tool_conditional_execution(&name, &args_json).await - }), - )) - .map_err(to_py_err)?; + py.detach(|| { + pyo3_async_runtimes::tokio::get_runtime().block_on( + py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_tool_api::tool_conditional_execution(&name, &args_json).await + }), + ), + ) + }) + .map_err(to_py_err)?; return Ok(py.None().into_bound(py)); } let scope_stack = current_scope_stack_handle(); @@ -1413,13 +1420,17 @@ fn llm_request_intercepts<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - let result = pyo3_async_runtimes::tokio::get_runtime() - .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_llm_api::llm_request_intercepts(&name, request.inner).await - }), - )) + let result = py + .detach(|| { + pyo3_async_runtimes::tokio::get_runtime().block_on( + py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_llm_api::llm_request_intercepts(&name, request.inner).await + }), + ), + ) + }) .map_err(to_py_err)?; return Py::new( py, @@ -1457,14 +1468,17 @@ fn llm_conditional_execution<'py>( .is_err() { let scope_stack = current_scope_stack_handle(); - pyo3_async_runtimes::tokio::get_runtime() - .block_on(py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_llm_api::llm_conditional_execution(&request.inner).await - }), - )) - .map_err(to_py_err)?; + py.detach(|| { + pyo3_async_runtimes::tokio::get_runtime().block_on( + py_callable::PY_AWAITABLES_ALLOWED.scope( + false, + TASK_SCOPE_STACK.scope(scope_stack, async move { + core_llm_api::llm_conditional_execution(&request.inner).await + }), + ), + ) + }) + .map_err(to_py_err)?; return Ok(py.None().into_bound(py)); } let scope_stack = current_scope_stack_handle(); diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index 3da6c3d68..b956d8f64 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -4,6 +4,7 @@ """Tests for NeMo Relay LLM lifecycle, guardrails, intercepts, and streaming.""" import asyncio +from collections.abc import AsyncIterator from typing import NoReturn, cast import pytest @@ -670,13 +671,13 @@ async def test_async_response_sanitizer_runs_during_stream_finalization(self): originating_loop = asyncio.get_running_loop() subscribers.register("py_llm_async_stream_sanitizer_sub", events.append) - async def sanitize_response(response, context): + async def sanitize_response(response, context) -> dict: del context await asyncio.sleep(0) assert asyncio.get_running_loop() is originating_loop return {"sanitized": response["raw"]} - async def stream_func(request): + async def stream_func(request) -> AsyncIterator[dict]: del request yield {"token": "hello"} @@ -686,7 +687,7 @@ async def stream_func(request): "stream_async_response_sanitizer", make_request(), stream_func, - lambda chunk: None, + lambda _chunk: None, lambda: {"raw": True}, ) assert [chunk async for chunk in stream] == [{"token": "hello"}] From 8f2d41743ed2e9ed92435ed9f91b445540a64614 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 19:39:41 -0400 Subject: [PATCH 29/83] fix: prevent async sanitizer flush deadlocks Signed-off-by: Will Killian --- crates/core/src/api/runtime.rs | 2 +- .../src/api/runtime/subscriber_dispatcher.rs | 39 +++++++++++++++++-- crates/core/src/api/subscriber.rs | 7 ++++ crates/ffi/nemo_relay.h | 6 +-- crates/ffi/src/api/llm_registry.rs | 8 ++-- crates/node/src/api/mod.rs | 8 ++-- crates/node/tests/event_sanitizers_tests.mjs | 19 +++++++++ crates/plugin/tests/typed_callbacks.rs | 26 +++++++++++-- crates/python/src/py_api/mod.rs | 8 ++-- 9 files changed, 100 insertions(+), 23 deletions(-) diff --git a/crates/core/src/api/runtime.rs b/crates/core/src/api/runtime.rs index 77670c804..b2878cead 100644 --- a/crates/core/src/api/runtime.rs +++ b/crates/core/src/api/runtime.rs @@ -27,4 +27,4 @@ pub use scope_stack::{ task_scope_top, with_active_event_uuid, with_scope_stack, }; pub use state::NemoRelayContextState; -pub use subscriber_dispatcher::flush_subscribers; +pub use subscriber_dispatcher::{flush_subscribers, flush_subscribers_from_binding}; diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 1d46f3fe6..8f9f1b8e3 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -52,11 +52,29 @@ mod native { OnceLock::new(); static DISPATCHER_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); static SANITIZER_RUNTIME_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); + static DISPATCH_IN_PROGRESS: AtomicBool = AtomicBool::new(false); thread_local! { static IN_DISPATCHER: Cell = const { Cell::new(false) }; } + struct DispatchGuard; + + impl DispatchGuard { + fn enter() -> Self { + debug_assert!(!DISPATCH_IN_PROGRESS.swap(true, Ordering::AcqRel)); + IN_DISPATCHER.with(|flag| flag.set(true)); + Self + } + } + + impl Drop for DispatchGuard { + fn drop(&mut self) { + IN_DISPATCHER.with(|flag| flag.set(false)); + DISPATCH_IN_PROGRESS.store(false, Ordering::Release); + } + } + fn sanitizer_runtime() -> std::result::Result<&'static tokio::runtime::Runtime, String> { SANITIZER_RUNTIME .get_or_init(|| { @@ -184,6 +202,13 @@ mod native { Ok(()) } + pub(super) fn flush_subscribers_from_binding() -> Result<()> { + if DISPATCH_IN_PROGRESS.load(Ordering::Acquire) { + return Ok(()); + } + flush_subscribers() + } + fn dispatcher_sender() -> std::result::Result, String> { DISPATCHER.get_or_init(start_dispatcher).clone() } @@ -288,9 +313,8 @@ mod native { ) { let previous_scope_stack = capture_thread_scope_stack(); set_thread_scope_stack(scope_stack); - IN_DISPATCHER.with(|flag| flag.set(true)); + let _dispatch_guard = DispatchGuard::enter(); let Some(event) = sanitize_event_snapshot(*event, transform, sanitizers) else { - IN_DISPATCHER.with(|flag| flag.set(false)); restore_thread_scope_stack(previous_scope_stack); return; }; @@ -303,7 +327,6 @@ mod native { ); } } - IN_DISPATCHER.with(|flag| flag.set(false)); restore_thread_scope_stack(previous_scope_stack); } @@ -417,3 +440,13 @@ pub(crate) fn register_async_publication() -> Option pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() } + +/// Wait for queued subscriber callbacks without creating a binding callback cycle. +/// +/// Language callbacks may resume on a different thread from the dispatcher. In that case the +/// thread-local reentrancy guard is insufficient, so bindings return early whenever an event is +/// actively being dispatched. +#[doc(hidden)] +pub fn flush_subscribers_from_binding() -> Result<()> { + native::flush_subscribers_from_binding() +} diff --git a/crates/core/src/api/subscriber.rs b/crates/core/src/api/subscriber.rs index 02ce9a322..a8c75c922 100644 --- a/crates/core/src/api/subscriber.rs +++ b/crates/core/src/api/subscriber.rs @@ -85,6 +85,13 @@ pub fn flush_subscribers() -> Result<()> { flush_runtime_subscribers() } +/// Binding-specific subscriber barrier that avoids cycles across asynchronous callback handoffs. +#[doc(hidden)] +pub fn flush_subscribers_from_binding() -> Result<()> { + ensure_runtime_owner()?; + crate::api::runtime::flush_subscribers_from_binding() +} + /// Register a scope-local lifecycle event subscriber. /// /// The subscriber remains active only while the target scope is still present diff --git a/crates/ffi/nemo_relay.h b/crates/ffi/nemo_relay.h index 70c480b24..7e4ea851f 100644 --- a/crates/ffi/nemo_relay.h +++ b/crates/ffi/nemo_relay.h @@ -1258,9 +1258,9 @@ NemoRelayStatus nemo_relay_deregister_subscriber(const char *name); /** * Wait for subscriber callbacks queued before this call to finish. * - * Call this function outside native subscriber callbacks. A re-entrant call returns without - * waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can - * still run. + * If publication is currently executing, this function returns without waiting. This prevents + * subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial + * dispatcher. */ NemoRelayStatus nemo_relay_flush_subscribers(void); diff --git a/crates/ffi/src/api/llm_registry.rs b/crates/ffi/src/api/llm_registry.rs index 38883ecee..486da1221 100644 --- a/crates/ffi/src/api/llm_registry.rs +++ b/crates/ffi/src/api/llm_registry.rs @@ -391,13 +391,13 @@ pub unsafe extern "C" fn nemo_relay_deregister_subscriber(name: *const c_char) - /// Wait for subscriber callbacks queued before this call to finish. /// -/// Call this function outside native subscriber callbacks. A re-entrant call returns without -/// waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can -/// still run. +/// If publication is currently executing, this function returns without waiting. This prevents +/// subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial +/// dispatcher. #[unsafe(no_mangle)] pub extern "C" fn nemo_relay_flush_subscribers() -> NemoRelayStatus { clear_last_error(); - match core_subscriber_api::flush_subscribers() { + match core_subscriber_api::flush_subscribers_from_binding() { Ok(()) => NemoRelayStatus::Ok, Err(e) => status_from_error(&e), } diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 2efa65892..daed96761 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -3172,9 +3172,9 @@ pub fn deregister_subscriber(name: String) -> Result { /// Return a Promise that resolves when native subscriber callbacks queued /// before this call finish. /// -/// Call this function outside native subscriber callbacks. A re-entrant call returns without -/// waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can -/// still run. +/// If publication is currently executing, this Promise resolves without waiting. This prevents +/// subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial +/// dispatcher. /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this /// Promise does not block the Node event loop while Promise-returning event sanitizers settle. @@ -3183,7 +3183,7 @@ pub fn deregister_subscriber(name: String) -> Result { /// Callers should handle errors when awaiting it. #[napi] pub async fn flush_subscribers() -> Result<()> { - tokio::task::spawn_blocking(core_subscriber_api::flush_subscribers) + tokio::task::spawn_blocking(core_subscriber_api::flush_subscribers_from_binding) .await .map_err(|error| to_napi_err(FlowError::Internal(error.to_string())))? .map_err(to_napi_err) diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index c5d6d75ea..e437d9574 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -129,6 +129,25 @@ describe('event sanitizer registries', () => { assert.deepEqual(events.at(-1).data, { sanitized: true }); }); + it('does not deadlock when an async sanitizer flushes subscribers', async () => { + const events = capture('node-event-sanitize-reentrant-flush-sub'); + let flushReturned = false; + lib.registerMarkSanitizeGuardrail('node-event-reentrant-flush', 0, async (_event, fields) => { + await lib.flushSubscribers(); + flushReturned = true; + return fields; + }); + try { + lib.event('reentrant-flush-checkpoint', null, { raw: true }); + await lib.flushSubscribers(); + await waitFor(events, 1); + } finally { + lib.deregisterMarkSanitizeGuardrail('node-event-reentrant-flush'); + lib.deregisterSubscriber('node-event-sanitize-reentrant-flush-sub'); + } + assert.equal(flushReturned, true); + }); + it('fails open and records invalid sanitizer results', async () => { const events = capture('node-event-sanitize-invalid-sub'); const invalidResults = { diff --git a/crates/plugin/tests/typed_callbacks.rs b/crates/plugin/tests/typed_callbacks.rs index eccca24aa..a56b2fdc6 100644 --- a/crates/plugin/tests/typed_callbacks.rs +++ b/crates/plugin/tests/typed_callbacks.rs @@ -35,10 +35,28 @@ use serde_json::{Map, json}; #[test] fn async_abi_discriminants_reject_unknown_values() { - assert_eq!( - NemoRelayNativeAsyncMiddlewareKind::try_from(13), - Ok(NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd) - ); + use NemoRelayNativeAsyncMiddlewareKind as Kind; + + let middleware_kinds = [ + Kind::ToolSanitizeRequest, + Kind::ToolSanitizeResponse, + Kind::ToolConditionalExecution, + Kind::ToolRequestIntercept, + Kind::ToolExecutionIntercept, + Kind::LlmSanitizeRequest, + Kind::LlmSanitizeResponse, + Kind::LlmConditionalExecution, + Kind::LlmRequestIntercept, + Kind::LlmExecutionIntercept, + Kind::LlmStreamExecutionIntercept, + Kind::MarkSanitize, + Kind::ScopeSanitizeStart, + Kind::ScopeSanitizeEnd, + ]; + for (discriminant, kind) in middleware_kinds.into_iter().enumerate() { + assert_eq!(kind as u32, discriminant as u32); + assert_eq!(Kind::try_from(discriminant as u32), Ok(kind)); + } assert!(NemoRelayNativeAsyncMiddlewareKind::try_from(14).is_err()); assert_eq!( NemoRelayNativeAsyncCallbackState::try_from(1), diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 534f68c99..678ed0e41 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1524,12 +1524,12 @@ fn deregister_subscriber(name: &str) -> PyResult { /// Wait for subscriber callbacks queued before this call to finish. /// -/// Call this function outside native subscriber callbacks. A re-entrant call returns without -/// waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can -/// still run. +/// If publication is currently executing, this function returns without waiting. This prevents +/// subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial +/// dispatcher. #[pyfunction] fn flush_subscribers(py: Python<'_>) -> PyResult<()> { - py.detach(core_subscriber_api::flush_subscribers) + py.detach(core_subscriber_api::flush_subscribers_from_binding) .map_err(to_py_err) } From 8b60b5dd48f453726ea351162c5e2751cce3f62e Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 19:46:04 -0400 Subject: [PATCH 30/83] fix: scope reentrant flush guards to callbacks Signed-off-by: Will Killian --- crates/core/src/api/runtime.rs | 2 +- .../src/api/runtime/subscriber_dispatcher.rs | 21 ----------------- crates/core/src/api/subscriber.rs | 7 ------ crates/ffi/nemo_relay.h | 6 ++--- crates/ffi/src/api/llm_registry.rs | 8 +++---- crates/node/src/api/mod.rs | 10 ++++---- crates/node/src/callable.rs | 23 +++++++++++++++++++ crates/python/src/py_api/mod.rs | 10 ++++---- crates/python/src/py_callable.rs | 23 +++++++++++++++++++ 9 files changed, 66 insertions(+), 44 deletions(-) diff --git a/crates/core/src/api/runtime.rs b/crates/core/src/api/runtime.rs index b2878cead..77670c804 100644 --- a/crates/core/src/api/runtime.rs +++ b/crates/core/src/api/runtime.rs @@ -27,4 +27,4 @@ pub use scope_stack::{ task_scope_top, with_active_event_uuid, with_scope_stack, }; pub use state::NemoRelayContextState; -pub use subscriber_dispatcher::{flush_subscribers, flush_subscribers_from_binding}; +pub use subscriber_dispatcher::flush_subscribers; diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 8f9f1b8e3..47dace406 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -52,8 +52,6 @@ mod native { OnceLock::new(); static DISPATCHER_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); static SANITIZER_RUNTIME_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); - static DISPATCH_IN_PROGRESS: AtomicBool = AtomicBool::new(false); - thread_local! { static IN_DISPATCHER: Cell = const { Cell::new(false) }; } @@ -62,7 +60,6 @@ mod native { impl DispatchGuard { fn enter() -> Self { - debug_assert!(!DISPATCH_IN_PROGRESS.swap(true, Ordering::AcqRel)); IN_DISPATCHER.with(|flag| flag.set(true)); Self } @@ -71,7 +68,6 @@ mod native { impl Drop for DispatchGuard { fn drop(&mut self) { IN_DISPATCHER.with(|flag| flag.set(false)); - DISPATCH_IN_PROGRESS.store(false, Ordering::Release); } } @@ -202,13 +198,6 @@ mod native { Ok(()) } - pub(super) fn flush_subscribers_from_binding() -> Result<()> { - if DISPATCH_IN_PROGRESS.load(Ordering::Acquire) { - return Ok(()); - } - flush_subscribers() - } - fn dispatcher_sender() -> std::result::Result, String> { DISPATCHER.get_or_init(start_dispatcher).clone() } @@ -440,13 +429,3 @@ pub(crate) fn register_async_publication() -> Option pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() } - -/// Wait for queued subscriber callbacks without creating a binding callback cycle. -/// -/// Language callbacks may resume on a different thread from the dispatcher. In that case the -/// thread-local reentrancy guard is insufficient, so bindings return early whenever an event is -/// actively being dispatched. -#[doc(hidden)] -pub fn flush_subscribers_from_binding() -> Result<()> { - native::flush_subscribers_from_binding() -} diff --git a/crates/core/src/api/subscriber.rs b/crates/core/src/api/subscriber.rs index a8c75c922..02ce9a322 100644 --- a/crates/core/src/api/subscriber.rs +++ b/crates/core/src/api/subscriber.rs @@ -85,13 +85,6 @@ pub fn flush_subscribers() -> Result<()> { flush_runtime_subscribers() } -/// Binding-specific subscriber barrier that avoids cycles across asynchronous callback handoffs. -#[doc(hidden)] -pub fn flush_subscribers_from_binding() -> Result<()> { - ensure_runtime_owner()?; - crate::api::runtime::flush_subscribers_from_binding() -} - /// Register a scope-local lifecycle event subscriber. /// /// The subscriber remains active only while the target scope is still present diff --git a/crates/ffi/nemo_relay.h b/crates/ffi/nemo_relay.h index 7e4ea851f..70c480b24 100644 --- a/crates/ffi/nemo_relay.h +++ b/crates/ffi/nemo_relay.h @@ -1258,9 +1258,9 @@ NemoRelayStatus nemo_relay_deregister_subscriber(const char *name); /** * Wait for subscriber callbacks queued before this call to finish. * - * If publication is currently executing, this function returns without waiting. This prevents - * subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial - * dispatcher. + * Call this function outside native subscriber callbacks. A re-entrant call returns without + * waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can + * still run. */ NemoRelayStatus nemo_relay_flush_subscribers(void); diff --git a/crates/ffi/src/api/llm_registry.rs b/crates/ffi/src/api/llm_registry.rs index 486da1221..38883ecee 100644 --- a/crates/ffi/src/api/llm_registry.rs +++ b/crates/ffi/src/api/llm_registry.rs @@ -391,13 +391,13 @@ pub unsafe extern "C" fn nemo_relay_deregister_subscriber(name: *const c_char) - /// Wait for subscriber callbacks queued before this call to finish. /// -/// If publication is currently executing, this function returns without waiting. This prevents -/// subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial -/// dispatcher. +/// Call this function outside native subscriber callbacks. A re-entrant call returns without +/// waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can +/// still run. #[unsafe(no_mangle)] pub extern "C" fn nemo_relay_flush_subscribers() -> NemoRelayStatus { clear_last_error(); - match core_subscriber_api::flush_subscribers_from_binding() { + match core_subscriber_api::flush_subscribers() { Ok(()) => NemoRelayStatus::Ok, Err(e) => status_from_error(&e), } diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index daed96761..6e22c3dc9 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -3172,9 +3172,8 @@ pub fn deregister_subscriber(name: String) -> Result { /// Return a Promise that resolves when native subscriber callbacks queued /// before this call finish. /// -/// If publication is currently executing, this Promise resolves without waiting. This prevents -/// subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial -/// dispatcher. +/// When called from an event-sanitizer callback, this Promise resolves without waiting to prevent +/// a cycle with the serial dispatcher. /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this /// Promise does not block the Node event loop while Promise-returning event sanitizers settle. @@ -3183,7 +3182,10 @@ pub fn deregister_subscriber(name: String) -> Result { /// Callers should handle errors when awaiting it. #[napi] pub async fn flush_subscribers() -> Result<()> { - tokio::task::spawn_blocking(core_subscriber_api::flush_subscribers_from_binding) + if crate::callable::event_sanitizer_callback_active() { + return Ok(()); + } + tokio::task::spawn_blocking(core_subscriber_api::flush_subscribers) .await .map_err(|error| to_napi_err(FlowError::Internal(error.to_string())))? .map_err(to_napi_err) diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index 4a7eb2a58..b541da082 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -12,6 +12,7 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use napi::bindgen_prelude::ToNapiValue; use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}; @@ -44,6 +45,27 @@ use crate::convert::{callback_json, record_callback_error, to_napi_err}; use crate::promise_call::{JsonNextFn, JsonStreamNextFn, PromiseAwareFn}; use crate::types::{EventSanitizeFields, JsEvent, event_sanitize_fields_from_json}; +static ACTIVE_EVENT_SANITIZER_CALLBACKS: AtomicUsize = AtomicUsize::new(0); + +struct ActiveEventSanitizerCallback; + +impl ActiveEventSanitizerCallback { + fn enter() -> Self { + ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_add(1, Ordering::AcqRel); + Self + } +} + +impl Drop for ActiveEventSanitizerCallback { + fn drop(&mut self) { + ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_sub(1, Ordering::AcqRel); + } +} + +pub(crate) fn event_sanitizer_callback_active() -> bool { + ACTIVE_EVENT_SANITIZER_CALLBACKS.load(Ordering::Acquire) != 0 +} + /// Structured codec identity delivered to JavaScript LLM sanitizers. #[napi(object)] #[derive(Clone)] @@ -476,6 +498,7 @@ pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSani Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); Box::pin(async move { + let _active_callback = ActiveEventSanitizerCallback::enter(); let event_json = JsEvent::try_from_event(&event) .map(JsEvent::into_json) .map_err(|error| { diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 678ed0e41..70d4c0742 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1524,12 +1524,14 @@ fn deregister_subscriber(name: &str) -> PyResult { /// Wait for subscriber callbacks queued before this call to finish. /// -/// If publication is currently executing, this function returns without waiting. This prevents -/// subscriber and asynchronous event-sanitizer callbacks from creating a cycle with the serial -/// dispatcher. +/// A call from an asynchronous event-sanitizer callback returns without waiting to prevent a +/// cycle with the serial dispatcher. #[pyfunction] fn flush_subscribers(py: Python<'_>) -> PyResult<()> { - py.detach(core_subscriber_api::flush_subscribers_from_binding) + if crate::py_callable::event_sanitizer_callback_active() { + return Ok(()); + } + py.detach(core_subscriber_api::flush_subscribers) .map_err(to_py_err) } diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index bea95c16d..9089da478 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -23,6 +23,7 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Poll}; use nemo_relay::api::runtime::{ @@ -43,6 +44,27 @@ use nemo_relay::api::event::{Event, EventSanitizeFields}; use nemo_relay::api::llm::LlmRequest; use nemo_relay::api::tool::ToolExecutionInterceptOutcome; use nemo_relay::codec::request::AnnotatedLlmRequest as AnnotatedLLMRequest; + +static ACTIVE_EVENT_SANITIZER_CALLBACKS: AtomicUsize = AtomicUsize::new(0); + +struct ActiveEventSanitizerCallback; + +impl ActiveEventSanitizerCallback { + fn enter() -> Self { + ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_add(1, Ordering::AcqRel); + Self + } +} + +impl Drop for ActiveEventSanitizerCallback { + fn drop(&mut self) { + ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_sub(1, Ordering::AcqRel); + } +} + +pub(crate) fn event_sanitizer_callback_active() -> bool { + ACTIVE_EVENT_SANITIZER_CALLBACKS.load(Ordering::Acquire) != 0 +} use nemo_relay::codec::response::AnnotatedLlmResponse as AnnotatedLLMResponse; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; @@ -1142,6 +1164,7 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let py_fn = py_fn.clone(); let task_locals = task_locals.clone(); Box::pin(async move { + let _active_callback = ActiveEventSanitizerCallback::enter(); let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { let py_event = match event.as_ref() { From cbb23a7ce26b8bf8001be690190f829f349cdd0e Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 20:24:34 -0400 Subject: [PATCH 31/83] fix(python): scope sanitizer flush reentrancy Signed-off-by: Will Killian --- crates/python/src/py_api/mod.rs | 7 +--- crates/python/src/py_callable.rs | 33 ++++------------ python/nemo_relay/_event_sanitizer_context.py | 38 +++++++++++++++++++ python/nemo_relay/subscribers.py | 9 +++-- python/tests/test_event_sanitizers.py | 30 +++++++++++++++ 5 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 python/nemo_relay/_event_sanitizer_context.py diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 70d4c0742..7bb90a042 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1524,13 +1524,10 @@ fn deregister_subscriber(name: &str) -> PyResult { /// Wait for subscriber callbacks queued before this call to finish. /// -/// A call from an asynchronous event-sanitizer callback returns without waiting to prevent a -/// cycle with the serial dispatcher. +/// Public Python wrappers prevent re-entrant event-sanitizer callbacks from waiting on the serial +/// dispatcher. #[pyfunction] fn flush_subscribers(py: Python<'_>) -> PyResult<()> { - if crate::py_callable::event_sanitizer_callback_active() { - return Ok(()); - } py.detach(core_subscriber_api::flush_subscribers) .map_err(to_py_err) } diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 9089da478..cc89236d4 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -23,7 +23,6 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Poll}; use nemo_relay::api::runtime::{ @@ -44,27 +43,6 @@ use nemo_relay::api::event::{Event, EventSanitizeFields}; use nemo_relay::api::llm::LlmRequest; use nemo_relay::api::tool::ToolExecutionInterceptOutcome; use nemo_relay::codec::request::AnnotatedLlmRequest as AnnotatedLLMRequest; - -static ACTIVE_EVENT_SANITIZER_CALLBACKS: AtomicUsize = AtomicUsize::new(0); - -struct ActiveEventSanitizerCallback; - -impl ActiveEventSanitizerCallback { - fn enter() -> Self { - ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_add(1, Ordering::AcqRel); - Self - } -} - -impl Drop for ActiveEventSanitizerCallback { - fn drop(&mut self) { - ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_sub(1, Ordering::AcqRel); - } -} - -pub(crate) fn event_sanitizer_callback_active() -> bool { - ACTIVE_EVENT_SANITIZER_CALLBACKS.load(Ordering::Acquire) != 0 -} use nemo_relay::codec::response::AnnotatedLlmResponse as AnnotatedLLMResponse; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; @@ -1164,7 +1142,6 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let py_fn = py_fn.clone(); let task_locals = task_locals.clone(); Box::pin(async move { - let _active_callback = ActiveEventSanitizerCallback::enter(); let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { let py_event = match event.as_ref() { @@ -1201,10 +1178,14 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { return Err(FlowError::Internal(error.to_string())); } }; - let result = py_fn - .call1(py, (py_event, py_fields)) + let invoke = py + .import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("invoke")) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = invoke + .call1((py_fn.bind(py), py_event, py_fields)) .map_err(|error| FlowError::Internal(error.to_string()))?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) + split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) }, ); let result = resolve_py_object_or_future(result).await?; diff --git a/python/nemo_relay/_event_sanitizer_context.py b/python/nemo_relay/_event_sanitizer_context.py new file mode 100644 index 000000000..f2cc89a09 --- /dev/null +++ b/python/nemo_relay/_event_sanitizer_context.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Track re-entrant subscriber flushes from Python event sanitizers.""" + +from __future__ import annotations + +import inspect +from collections.abc import Awaitable, Callable +from contextvars import ContextVar +from typing import Any + +_ACTIVE: ContextVar[bool] = ContextVar("nemo_relay_event_sanitizer_active", default=False) + + +def callback_active() -> bool: + """Return whether the current Python context is running an event sanitizer.""" + return _ACTIVE.get() + + +async def _await_result(result: Awaitable[Any]) -> Any: + token = _ACTIVE.set(True) + try: + return await result + finally: + _ACTIVE.reset(token) + + +def invoke(callback: Callable[..., Any], *args: Any) -> Any: + """Invoke a sanitizer while marking its sync and async execution contexts.""" + token = _ACTIVE.set(True) + try: + result = callback(*args) + finally: + _ACTIVE.reset(token) + if inspect.isawaitable(result): + return _await_result(result) + return result diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index b54b42e2e..d5a60cc7c 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -25,6 +25,7 @@ def log_event(event): from collections.abc import Callable from typing import TYPE_CHECKING +from nemo_relay._event_sanitizer_context import callback_active as _event_sanitizer_callback_active from nemo_relay._native import ( deregister_subscriber as _native_deregister, ) @@ -94,10 +95,12 @@ def flush() -> None: waiting for observer work. Use this barrier in tests and shutdown paths when captured subscriber output must be complete before continuing. - Call this function outside subscriber callbacks. A re-entrant call returns - without waiting to avoid blocking the dispatcher, so callbacks later in the - same dispatch snapshot can still run. + Call this function outside subscriber and event-sanitizer callbacks. A + re-entrant call returns without waiting to avoid blocking the dispatcher, + so callbacks later in the same dispatch snapshot can still run. """ + if _event_sanitizer_callback_active(): + return None return _native_flush() diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 4a91bf658..c0dc3c78e 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -97,6 +97,36 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert events[-1].data == {"async": True} +@pytest.mark.parametrize("asynchronous", [False, True]) +async def test_event_sanitizer_flush_is_reentrant(capture_events, asynchronous): + _capture_name, events = capture_events + flush_returned = False + + def sanitize_sync(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + nonlocal flush_returned + subscribers.flush() + flush_returned = True + return fields + + async def sanitize_async(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + await asyncio.sleep(0) + return sanitize_sync(_event, fields) + + guardrails.register_mark_sanitize( + "python-reentrant-mark", + 0, + sanitize_async if asynchronous else sanitize_sync, + ) + try: + scope.event("reentrant-checkpoint", data={"raw": True}) + await asyncio.to_thread(subscribers.flush) + finally: + guardrails.deregister_mark_sanitize("python-reentrant-mark") + + assert flush_returned is True + assert events[-1].data == {"raw": True} + + def test_scope_start_and_end_sanitizers_cover_category_profile(capture_events): _capture_name, events = capture_events From bd8099ac1efe3b4a8ec528222dc5bbfc29917d56 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 21:07:00 -0400 Subject: [PATCH 32/83] fix: address async middleware review regressions Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 8 +++- crates/core/src/api/tool.rs | 8 +++- .../tests/integration/api_surface_tests.rs | 22 +++++----- .../tests/integration/native_plugin_tests.rs | 2 + crates/core/tests/unit/shared_tests.rs | 26 ++++++++--- crates/plugin/README.md | 5 ++- crates/python/src/py_callable.rs | 4 +- .../python/tests/coverage/coverage_tests.rs | 44 +++++++++++-------- .../coverage/py_callable_coverage_tests.rs | 31 ++++++++++++- 9 files changed, 106 insertions(+), 44 deletions(-) diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index 0301aeeda..b6a7ffa52 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -719,7 +719,9 @@ pub fn llm_call(params: LlmCallParams<'_>) -> Result { let handle = create_llm_handle(handle_params)?; let scope_stack = handle.captured_scope_stack().clone(); let (entries, subscribers, agent_is_fresh) = { - let mut scope_guard = scope_stack.write().expect("scope stack lock poisoned"); + let mut scope_guard = scope_stack + .write() + .map_err(|error| FlowError::Internal(error.to_string()))?; let scope_locals = scope_guard.collect_scope_local_registries(|registries| { ®istries.llm_sanitize_request_guardrails }); @@ -899,7 +901,9 @@ pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { ensure_runtime_owner()?; let scope_stack = params.handle.captured_scope_stack().clone(); let (entries, subscribers) = { - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_guard = scope_stack + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; let scope_locals = scope_guard.collect_scope_local_registries(|registries| { ®istries.llm_sanitize_response_guardrails }); diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 671471748..30a9249e1 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -231,7 +231,9 @@ pub fn tool_call(params: ToolCallParams<'_>) -> Result { ensure_runtime_owner()?; let scope_stack = current_scope_stack(); let (entries, subscribers) = { - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_guard = scope_stack + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; let scope_locals = scope_guard.collect_scope_local_registries(|registries| { ®istries.tool_sanitize_request_guardrails }); @@ -418,7 +420,9 @@ pub fn tool_call_end(params: ToolCallEndParams<'_>) -> Result<()> { ensure_runtime_owner()?; let scope_stack = current_scope_stack(); let (entries, subscribers) = { - let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); + let scope_guard = scope_stack + .read() + .map_err(|error| FlowError::Internal(error.to_string()))?; let scope_locals = scope_guard.collect_scope_local_registries(|registries| { ®istries.tool_sanitize_response_guardrails }); diff --git a/crates/core/tests/integration/api_surface_tests.rs b/crates/core/tests/integration/api_surface_tests.rs index 61f56251f..5b14fe82f 100644 --- a/crates/core/tests/integration/api_surface_tests.rs +++ b/crates/core/tests/integration/api_surface_tests.rs @@ -1921,21 +1921,19 @@ async fn test_llm_stream_api_covers_success_rejection_and_execution_error_paths( stream.close().await.unwrap(); let success_events = captured_events_snapshot(&events); - let success_start = success_events + let scope_events = success_events .iter() - .find(|event| { - event.kind() == "scope" && event.scope_category() == Some(ScopeCategory::Start) - }) - .expect("stream start event"); - let success_end = success_events - .iter() - .rev() - .find(|event| event.kind() == "scope" && event.scope_category() == Some(ScopeCategory::End)) - .expect("stream end event"); - assert_eq!(success_start.kind(), "scope"); + .filter(|event| event.kind() == "scope") + .collect::>(); + assert_eq!( + scope_events.len(), + 2, + "expected exactly one stream scope pair" + ); + let success_start = scope_events[0]; + let success_end = scope_events[1]; assert_eq!(success_start.scope_category(), Some(ScopeCategory::Start)); assert_eq!(success_start.category().unwrap().as_str(), "llm"); - assert_eq!(success_end.kind(), "scope"); assert_eq!(success_end.scope_category(), Some(ScopeCategory::End)); assert_eq!(success_end.category().unwrap().as_str(), "llm"); assert_eq!( diff --git a/crates/core/tests/integration/native_plugin_tests.rs b/crates/core/tests/integration/native_plugin_tests.rs index 91a07a4cf..8e9782def 100644 --- a/crates/core/tests/integration/native_plugin_tests.rs +++ b/crates/core/tests/integration/native_plugin_tests.rs @@ -680,6 +680,8 @@ async fn native_v3_async_registration_supports_all_middleware_kinds() { .get:: bool>(b"nemo_relay_fixture_async_pending_entered\0") .expect("v3 async native fixture should export its pending-entry signal") }; + // This pointer remains valid only while `activation` keeps the fixture + // library loaded; never call it after clearing the plugin configuration. assert!(!unsafe { pending_entered() }); drop(fixture_library); let mut cleanup = NativePluginTestCleanup::new(); diff --git a/crates/core/tests/unit/shared_tests.rs b/crates/core/tests/unit/shared_tests.rs index 1de4a9ed7..5375f4334 100644 --- a/crates/core/tests/unit/shared_tests.rs +++ b/crates/core/tests/unit/shared_tests.rs @@ -4,7 +4,7 @@ //! Unit tests for shared in the NeMo Relay core crate. use super::*; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use serde_json::{Map, json}; @@ -174,13 +174,16 @@ async fn test_run_request_intercepts_with_codec_none_and_codec_paths() { let _guard = lock_runtime_owner(); reset_global(); + let observed_without_codec = Arc::new(Mutex::new(None)); + let callback_observed_without_codec = Arc::clone(&observed_without_codec); register_llm_request_intercept( "shared-none", 1, false, - Arc::new(|_name, mut request, annotated| { + Arc::new(move |_name, mut request, annotated| { + let callback_observed_without_codec = Arc::clone(&callback_observed_without_codec); Box::pin(async move { - assert!(annotated.is_none()); + *callback_observed_without_codec.lock().unwrap() = Some(annotated.is_none()); request.headers.insert("x-no-codec".into(), json!(true)); let mut annotated = SharedTestCodec.decode(&request)?; annotated.model = Some("interceptor-model".into()); @@ -201,6 +204,7 @@ async fn test_run_request_intercepts_with_codec_none_and_codec_paths() { ) .await .unwrap(); + assert_eq!(*observed_without_codec.lock().unwrap(), Some(true)); assert_eq!( request_without_codec.headers.get("x-no-codec"), Some(&json!(true)) @@ -214,13 +218,21 @@ async fn test_run_request_intercepts_with_codec_none_and_codec_paths() { assert!(pending_marks_without_codec.is_empty()); deregister_llm_request_intercept("shared-none").unwrap(); + let observed_with_codec = Arc::new(Mutex::new(None)); + let callback_observed_with_codec = Arc::clone(&observed_with_codec); register_llm_request_intercept( "shared-codec", 1, false, - Arc::new(|_name, mut request, annotated| { + Arc::new(move |_name, mut request, annotated| { + let callback_observed_with_codec = Arc::clone(&callback_observed_with_codec); Box::pin(async move { - let mut annotated = annotated.expect("codec should provide annotated request"); + *callback_observed_with_codec.lock().unwrap() = + Some(annotated.as_ref().and_then(|value| value.model.clone())); + let mut annotated = match annotated { + Some(value) => value, + None => SharedTestCodec.decode(&request)?, + }; annotated.model = Some("intercepted-model".into()); request.headers.insert("x-codec".into(), json!(true)); Ok(LlmRequestInterceptOutcome::new(request, Some(annotated))) @@ -242,6 +254,10 @@ async fn test_run_request_intercepts_with_codec_none_and_codec_paths() { .await .unwrap(); + assert_eq!( + *observed_with_codec.lock().unwrap(), + Some(Some("decoded-model".into())) + ); assert_eq!( request_with_codec.headers.get("x-codec"), Some(&json!(true)) diff --git a/crates/plugin/README.md b/crates/plugin/README.md index 0b68a888e..ecafb951f 100644 --- a/crates/plugin/README.md +++ b/crates/plugin/README.md @@ -43,8 +43,9 @@ the dynamic-library boundary on the stable C-compatible ABI. subscribers. - **`PluginRuntime`**: Typed helpers for Relay-owned scopes and marks. - **Stable native ABI v3**: C-compatible host and plugin tables behind the - safe Rust authoring interface, with a v2-compatible prefix for existing - plugins. + safe Rust authoring interface. The v3 tables preserve a v2-compatible field + prefix, but native plugins must still be rebuilt for v3 as described in the + [0.7 migration guide](../../docs/reference/migration-guides.mdx#upgrade-to-nemo-relay-07). - **Raw async middleware**: Completion-based raw registrations for plugins that need asynchronous guardrails, intercepts, or event sanitizers. Typed Rust callbacks remain synchronous convenience APIs. diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index cc89236d4..2c51cfef8 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -1072,7 +1072,7 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon let task_locals = capture_python_task_locals(); Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { let py_fn = py_fn.clone(); - let task_locals = task_locals.clone(); + let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let py_context = PyLlmSanitizeResponseContext { inner: context }; @@ -1140,7 +1140,7 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let task_locals = capture_python_task_locals(); Arc::new(move |event: Arc, fields: EventSanitizeFields| { let py_fn = py_fn.clone(); - let task_locals = task_locals.clone(); + let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); Box::pin(async move { let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { diff --git a/crates/python/tests/coverage/coverage_tests.rs b/crates/python/tests/coverage/coverage_tests.rs index 2fa805abf..7eab65355 100644 --- a/crates/python/tests/coverage/coverage_tests.rs +++ b/crates/python/tests/coverage/coverage_tests.rs @@ -674,10 +674,12 @@ def event_fail(event): ); let tool_fail = wrap_py_tool_fn(module.getattr("tool_fail").unwrap().unbind()); + let error = runtime + .block_on(tool_fail("demo".to_string(), json!({"x": 1}))) + .unwrap_err(); assert!( - runtime - .block_on(tool_fail("demo".to_string(), json!({"x": 1}))) - .is_err() + error.to_string().contains("tool boom"), + "unexpected tool error: {error}" ); let tool_cond = @@ -694,13 +696,15 @@ def event_fail(event): let llm_sanitize = wrap_py_llm_sanitize_request_fn(module.getattr("llm_sanitize_bad").unwrap().unbind()) .unwrap(); + let error = runtime + .block_on(llm_sanitize( + request.clone(), + nemo_relay::api::runtime::LlmSanitizeRequestContext::default(), + )) + .unwrap_err(); assert!( - runtime - .block_on(llm_sanitize( - request.clone(), - nemo_relay::api::runtime::LlmSanitizeRequestContext::default(), - )) - .is_err() + error.to_string().contains("unexpected type"), + "unexpected LLM request sanitizer error: {error}" ); let llm_cond = wrap_py_llm_conditional_fn(module.getattr("llm_cond_bad").unwrap().unbind()); @@ -730,22 +734,26 @@ def event_fail(event): let tool_req = wrap_py_tool_request_intercept_fn(module.getattr("tool_fail").unwrap().unbind()); + let error = runtime + .block_on(tool_req("demo".to_string(), json!({"x": 1}))) + .unwrap_err(); assert!( - runtime - .block_on(tool_req("demo".to_string(), json!({"x": 1}))) - .is_err() + error.to_string().contains("tool boom"), + "unexpected tool request intercept error: {error}" ); let llm_resp = wrap_py_llm_sanitize_response_fn(module.getattr("llm_resp_fail").unwrap().unbind()) .unwrap(); + let error = runtime + .block_on(llm_resp( + json!({"ok": true}), + nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), + )) + .unwrap_err(); assert!( - runtime - .block_on(llm_resp( - json!({"ok": true}), - nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), - )) - .is_err() + error.to_string().contains("resp boom"), + "unexpected LLM response sanitizer error: {error}" ); let mut collector = diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index 53245133c..ccb6cef65 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -8,7 +8,7 @@ use super::*; use std::ffi::CString; use std::sync::Arc; -use pyo3::types::PyModule; +use pyo3::types::{PyDict, PyList, PyModule}; use serde_json::json; fn load_module<'py>(py: Python<'py>, code: &str) -> Bound<'py, PyModule> { @@ -18,6 +18,34 @@ fn load_module<'py>(py: Python<'py>, code: &str) -> Bound<'py, PyModule> { PyModule::from_code(py, &code, &file_name, &module_name).unwrap() } +fn install_event_sanitizer_context_module(py: Python<'_>) { + let code = CString::new(include_str!( + "../../../../python/nemo_relay/_event_sanitizer_context.py" + )) + .unwrap(); + let file_name = CString::new("_event_sanitizer_context.py").unwrap(); + let module_name = CString::new("nemo_relay._event_sanitizer_context").unwrap(); + let context = PyModule::from_code(py, &code, &file_name, &module_name).unwrap(); + let parent = PyModule::new(py, "nemo_relay").unwrap(); + parent + .setattr("__path__", PyList::empty(py)) + .expect("test package path should be writable"); + parent + .setattr("_event_sanitizer_context", &context) + .expect("test context module should be writable"); + let modules = py + .import("sys") + .unwrap() + .getattr("modules") + .unwrap() + .cast_into::() + .unwrap(); + modules.set_item("nemo_relay", parent).unwrap(); + modules + .set_item("nemo_relay._event_sanitizer_context", context) + .unwrap(); +} + fn make_request() -> LlmRequest { LlmRequest { headers: serde_json::Map::new(), @@ -673,6 +701,7 @@ fn event_sanitize_wrapper_covers_conversion_success_and_error_propagation() { let _python = crate::test_support::init_python_test(); Python::attach(|py| { + install_event_sanitizer_context_module(py); let module = load_module( py, r#" From d7ebb6743df1e087350f3a0a68007fdb25a63ad0 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 21:16:50 -0400 Subject: [PATCH 33/83] fix: address async binding review findings Signed-off-by: Will Killian --- .../adaptive/src/adaptive_hints_intercept.rs | 4 +- .../tests/fixtures/native_plugin/src/lib.rs | 232 +++++++++++++----- crates/ffi/src/callable.rs | 30 ++- crates/node/src/callable.rs | 1 + crates/python/src/py_callable.rs | 8 +- 5 files changed, 198 insertions(+), 77 deletions(-) diff --git a/crates/adaptive/src/adaptive_hints_intercept.rs b/crates/adaptive/src/adaptive_hints_intercept.rs index b4b649264..50437e4da 100644 --- a/crates/adaptive/src/adaptive_hints_intercept.rs +++ b/crates/adaptive/src/adaptive_hints_intercept.rs @@ -178,9 +178,9 @@ impl AdaptiveHintsIntercept { mut request: LlmRequest, mut annotated: Option| { let this = this.clone(); + let scope_path = extract_scope_path(); + let manual_ls = read_manual_latency_sensitivity(); Box::pin(async move { - let scope_path = extract_scope_path(); - let manual_ls = read_manual_latency_sensitivity(); let scope_depth = scope_path.len(); let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed); diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index 00d417117..46c05c265 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -7,11 +7,11 @@ use std::sync::atomic::{AtomicBool, Ordering}; use nemo_relay_plugin::{ CategoryProfile, ConfigDiagnostic, DiagnosticLevel, Event, EventCategory, EventSanitizeFields, - Json, LlmJsonStream, LlmRequest, LlmRequestInterceptOutcome, NemoRelayNativeHostApiV1, - NemoRelayNativeHostApiV3, NemoRelayNativeAsyncCallbackState, NemoRelayNativeAsyncMiddlewareCb, - NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativePluginContext, NemoRelayNativePluginV1, - NemoRelayNativeString, NemoRelayStatus, - NemoRelayNativeToolNextFn, NativePlugin, PendingMarkSpec, PluginContext, PluginRuntime, + Json, LlmJsonStream, LlmRequest, LlmRequestInterceptOutcome, NativePlugin, + NemoRelayNativeAsyncCallbackState, NemoRelayNativeAsyncMiddlewareCb, + NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativeHostApiV1, NemoRelayNativeHostApiV3, + NemoRelayNativePluginContext, NemoRelayNativePluginV1, NemoRelayNativeString, + NemoRelayNativeToolNextFn, NemoRelayStatus, PendingMarkSpec, PluginContext, PluginRuntime, ScopeCategory, ScopeType, ToolExecutionInterceptOutcome, }; use serde_json::{Map, json}; @@ -65,11 +65,9 @@ impl NativePlugin for FixtureNativePlugin { 0, |_, fields| mark_event_fields(fields, "native_plugin_scope_start"), )?; - ctx.register_scope_sanitize_end_guardrail( - "fixture_scope_end_sanitize", - 0, - |_, fields| mark_event_fields(fields, "native_plugin_scope_end"), - )?; + ctx.register_scope_sanitize_end_guardrail("fixture_scope_end_sanitize", 0, |_, fields| { + mark_event_fields(fields, "native_plugin_scope_end") + })?; ctx.register_tool_sanitize_request_guardrail( "fixture_tool_sanitize_request", @@ -140,7 +138,12 @@ impl NativePlugin for FixtureNativePlugin { ctx.register_llm_sanitize_request_guardrail( "fixture_llm_sanitize_request", 0, - |request, _context| Some(mark_llm_request(request, "native_plugin_llm_sanitize_request")), + |request, _context| { + Some(mark_llm_request( + request, + "native_plugin_llm_sanitize_request", + )) + }, )?; ctx.register_llm_sanitize_response_guardrail( "fixture_llm_sanitize_response", @@ -175,13 +178,17 @@ impl NativePlugin for FixtureNativePlugin { )) }, )?; - ctx.register_llm_execution_intercept("fixture_llm_execution", 0, |_name, request, next| { - let response = next.call(mark_llm_request( - request, - "native_plugin_llm_execution_request", - ))?; - Ok(mark_json(response, "native_plugin_llm_execution")) - })?; + ctx.register_llm_execution_intercept( + "fixture_llm_execution", + 0, + |_name, request, next| { + let response = next.call(mark_llm_request( + request, + "native_plugin_llm_execution_request", + ))?; + Ok(mark_json(response, "native_plugin_llm_execution")) + }, + )?; ctx.register_llm_stream_execution_intercept( "fixture_llm_stream_execution", 0, @@ -616,24 +623,81 @@ unsafe extern "C" fn raw_register_async_tool_request( return NemoRelayStatus::NullPointer; } let host = unsafe { &*(user_data as *const NemoRelayNativeHostApiV3) }; - let registrations: [ - (NemoRelayNativeAsyncMiddlewareKind, &str, NemoRelayNativeAsyncMiddlewareCb); - 14 - ] = [ - (NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest, "fixture_async_tool_sanitize_request", raw_async_passthrough_callback), - (NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse, "fixture_async_tool_sanitize_response", raw_async_passthrough_callback), - (NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution, "fixture_async_tool_conditional", raw_async_allow_callback), - (NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept, "fixture_async_request", raw_async_tool_request_callback), - (NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept, "fixture_async_execution", raw_async_tool_execution_callback), - (NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest, "fixture_async_llm_sanitize_request", raw_async_passthrough_callback), - (NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse, "fixture_async_llm_sanitize_response", raw_async_passthrough_callback), - (NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution, "fixture_async_llm_conditional", raw_async_allow_callback), - (NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept, "fixture_async_llm_request", raw_async_passthrough_callback), - (NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept, "fixture_async_llm_execution", raw_async_tool_execution_callback), - (NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept, "fixture_async_llm_stream", raw_async_tool_execution_callback), - (NemoRelayNativeAsyncMiddlewareKind::MarkSanitize, "fixture_async_mark", raw_async_passthrough_callback), - (NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart, "fixture_async_scope_start", raw_async_passthrough_callback), - (NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd, "fixture_async_scope_end", raw_async_passthrough_callback), + let registrations: [( + NemoRelayNativeAsyncMiddlewareKind, + &str, + NemoRelayNativeAsyncMiddlewareCb, + ); 14] = [ + ( + NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest, + "fixture_async_tool_sanitize_request", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse, + "fixture_async_tool_sanitize_response", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution, + "fixture_async_tool_conditional", + raw_async_allow_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept, + "fixture_async_request", + raw_async_tool_request_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept, + "fixture_async_execution", + raw_async_tool_execution_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest, + "fixture_async_llm_sanitize_request", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse, + "fixture_async_llm_sanitize_response", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution, + "fixture_async_llm_conditional", + raw_async_allow_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept, + "fixture_async_llm_request", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept, + "fixture_async_llm_execution", + raw_async_tool_execution_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept, + "fixture_async_llm_stream", + raw_async_tool_execution_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::MarkSanitize, + "fixture_async_mark", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart, + "fixture_async_scope_start", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd, + "fixture_async_scope_end", + raw_async_passthrough_callback, + ), ]; for (kind, registration_name, callback) in registrations { let name = unsafe { raw_host_string(&host.v1, registration_name) }; @@ -642,7 +706,14 @@ unsafe extern "C" fn raw_register_async_tool_request( } let status = unsafe { (host.plugin_context_register_async_middleware)( - ctx, kind as u32, name, 0, false, callback, user_data, None, + ctx, + kind as u32, + name, + 0, + false, + callback, + user_data, + None, ) }; unsafe { (host.v1.string_free)(name) }; @@ -664,7 +735,9 @@ unsafe extern "C" fn raw_async_allow_callback( }; let result = unsafe { raw_host_string(&host.v1, "null") }; if result.is_null() { - unsafe { reject_async_completion(host, completion, "failed to allocate async allow result") }; + unsafe { + reject_async_completion(host, completion, "failed to allocate async allow result") + }; } else { unsafe { (host.async_completion_resolve_json)(completion, result); @@ -686,27 +759,38 @@ unsafe extern "C" fn raw_async_passthrough_callback( let result = unsafe { raw_host_string_value(&host.v1, invocation_json) } .and_then(|value| serde_json::from_str::(&value).ok()) .and_then(|invocation| { - invocation.get("annotated").map(|annotated| { - json!({ - "request": invocation["request"], - "annotated_request": annotated, - "pending_marks": [], - "optimization_contributions": [], + invocation + .get("annotated") + .map(|annotated| { + json!({ + "request": invocation["request"], + "annotated_request": annotated, + "pending_marks": [], + "optimization_contributions": [], + }) + }) + .or_else(|| { + ["value", "request", "response", "fields"] + .into_iter() + .find_map(|key| invocation.get(key).cloned()) }) - }).or_else(|| { - ["value", "request", "response", "fields"] - .into_iter() - .find_map(|key| invocation.get(key).cloned()) - }) }) .and_then(|value| serde_json::to_string(&value).ok()); let Some(result) = result else { - unsafe { reject_async_completion(host, completion, "invalid async passthrough invocation") }; + unsafe { + reject_async_completion(host, completion, "invalid async passthrough invocation") + }; return NemoRelayNativeAsyncCallbackState::Complete as u32; }; let result = unsafe { raw_host_string(&host.v1, &result) }; if result.is_null() { - unsafe { reject_async_completion(host, completion, "failed to allocate async passthrough result") }; + unsafe { + reject_async_completion( + host, + completion, + "failed to allocate async passthrough result", + ) + }; } else { unsafe { (host.async_completion_resolve_json)(completion, result); @@ -744,7 +828,9 @@ unsafe extern "C" fn raw_async_tool_request_callback( .map(|value| (value, pending, duplicate)) }); let Some((result, pending, duplicate)) = invocation else { - unsafe { reject_async_completion(host, completion, "invalid async tool request invocation") }; + unsafe { + reject_async_completion(host, completion, "invalid async tool request invocation") + }; return NemoRelayNativeAsyncCallbackState::Complete as u32; }; if pending { @@ -790,7 +876,13 @@ unsafe extern "C" fn raw_async_tool_request_callback( (host.v1.string_free)(result); } } else { - unsafe { reject_async_completion(host, completion, "failed to allocate async tool request result") }; + unsafe { + reject_async_completion( + host, + completion, + "failed to allocate async tool request result", + ) + }; } NemoRelayNativeAsyncCallbackState::Complete as u32 } @@ -805,7 +897,13 @@ unsafe extern "C" fn raw_async_tool_execution_callback( return NemoRelayNativeAsyncCallbackState::Complete as u32; }; if next.is_null() || completion.is_null() { - unsafe { reject_async_completion(host, completion, "async tool execution requires next and completion") }; + unsafe { + reject_async_completion( + host, + completion, + "async tool execution requires next and completion", + ) + }; if !next.is_null() { unsafe { (host.async_next_release)(next) }; } @@ -823,13 +921,21 @@ unsafe extern "C" fn raw_async_tool_execution_callback( }) .and_then(|value| serde_json::to_string(&value).ok()); let Some(value) = value else { - unsafe { reject_async_completion(host, completion, "invalid async tool execution invocation") }; + unsafe { + reject_async_completion(host, completion, "invalid async tool execution invocation") + }; unsafe { (host.async_next_release)(next) }; return NemoRelayNativeAsyncCallbackState::Complete as u32; }; let value = unsafe { raw_host_string(&host.v1, &value) }; if value.is_null() { - unsafe { reject_async_completion(host, completion, "failed to allocate async tool execution invocation") }; + unsafe { + reject_async_completion( + host, + completion, + "failed to allocate async tool execution invocation", + ) + }; unsafe { (host.async_next_release)(next) }; return NemoRelayNativeAsyncCallbackState::Complete as u32; } @@ -844,7 +950,13 @@ unsafe extern "C" fn raw_async_tool_execution_callback( } NemoRelayNativeAsyncCallbackState::Pending as u32 } else { - unsafe { reject_async_completion(host, completion, "failed to invoke async tool execution next") }; + unsafe { + reject_async_completion( + host, + completion, + "failed to invoke async tool execution next", + ) + }; unsafe { (host.async_next_release)(next) }; NemoRelayNativeAsyncCallbackState::Complete as u32 } @@ -896,10 +1008,8 @@ unsafe extern "C" fn raw_tool_outcome_callback( } "fixture-status-error-outcome" => { unsafe { - *out_outcome_json = raw_host_string( - host, - r#"{"result":{"stale":true},"pending_marks":[]}"#, - ); + *out_outcome_json = + raw_host_string(host, r#"{"result":{"stale":true},"pending_marks":[]}"#); set_raw_last_error_from_user_data(user_data, "fixture tool execution failed"); } NemoRelayStatus::Internal diff --git a/crates/ffi/src/callable.rs b/crates/ffi/src/callable.rs index 0fc04fa9f..58d4c3f60 100644 --- a/crates/ffi/src/callable.rs +++ b/crates/ffi/src/callable.rs @@ -324,13 +324,14 @@ pub fn wrap_tool_sanitize_fn( Arc::new(move |name: String, args: Json| { let ud = ud.clone(); Box::pin(async move { + clear_last_error(); let c_name = CString::new(name).unwrap_or_default(); let c_args = json_to_c_string(&args); let result_ptr = unsafe { cb(ud.ptr, c_name.as_ptr(), c_args) }; unsafe { nemo_relay_string_free_internal(c_args) }; - let result = ptr_to_json(result_ptr); + let result = json_result_from_ptr(result_ptr, "tool sanitize callback returned null"); unsafe { nemo_relay_string_free_internal(result_ptr) }; - Ok(result) + result }) }) } @@ -400,9 +401,9 @@ pub fn wrap_tool_exec_fn( let c_args = json_to_c_string(&args); let result_ptr = unsafe { cb(ud.ptr, c_args) }; unsafe { nemo_relay_string_free_internal(c_args) }; - let result = json_result_from_ptr(result_ptr, "tool execution callback failed")?; + let result = json_result_from_ptr(result_ptr, "tool execution callback failed"); unsafe { nemo_relay_string_free_internal(result_ptr) }; - Ok(result) + result }) }) } @@ -457,8 +458,9 @@ pub fn wrap_tool_exec_intercept_fn( unsafe { drop(Box::from_raw(next_ctx as *mut ToolExecutionNextFn)) }; unsafe { nemo_relay_string_free_internal(c_args) }; let outcome_json = - json_result_from_ptr(result_ptr, "tool execution intercept callback failed")?; + json_result_from_ptr(result_ptr, "tool execution intercept callback failed"); unsafe { nemo_relay_string_free_internal(result_ptr) }; + let outcome_json = outcome_json?; serde_json::from_value::(outcome_json).map_err(|error| { FlowError::Internal(format!( "invalid tool execution intercept outcome JSON: {error}" @@ -528,9 +530,9 @@ pub fn wrap_llm_exec_intercept_fn( unsafe { drop(Box::from_raw(next_ctx as *mut LlmExecutionNextFn)) }; unsafe { nemo_relay_string_free_internal(c_request) }; let result = - json_result_from_ptr(result_ptr, "LLM execution intercept callback failed")?; + json_result_from_ptr(result_ptr, "LLM execution intercept callback failed"); unsafe { nemo_relay_string_free_internal(result_ptr) }; - Ok(result) + result }) }, ) @@ -606,8 +608,9 @@ pub fn wrap_llm_stream_exec_intercept_fn( let result = json_result_from_ptr( result_ptr, "LLM stream execution intercept callback failed", - )?; + ); unsafe { nemo_relay_string_free_internal(result_ptr) }; + let result = result?; let stream = tokio_stream::once(Ok(result)); Ok(LlmJsonStream::new(stream)) }) @@ -843,9 +846,9 @@ pub fn wrap_llm_exec_fn( let c_request = json_to_c_string(&request_json); let result_ptr = unsafe { cb(ud.ptr, c_request) }; unsafe { nemo_relay_string_free_internal(c_request) }; - let result = json_result_from_ptr(result_ptr, "LLM execution callback failed")?; + let result = json_result_from_ptr(result_ptr, "LLM execution callback failed"); unsafe { nemo_relay_string_free_internal(result_ptr) }; - Ok(result) + result }) }) } @@ -868,8 +871,9 @@ pub fn wrap_llm_stream_exec_fn( let c_request = json_to_c_string(&request_json); let result_ptr = unsafe { cb(ud.ptr, c_request) }; unsafe { nemo_relay_string_free_internal(c_request) }; - let result = json_result_from_ptr(result_ptr, "LLM stream execution callback failed")?; + let result = json_result_from_ptr(result_ptr, "LLM stream execution callback failed"); unsafe { nemo_relay_string_free_internal(result_ptr) }; + let result = result?; // The C callback returns the full response as a single JSON value for stream // We emit it as a single-item stream let stream = tokio_stream::once(Ok(result)); @@ -1052,7 +1056,9 @@ fn json_result_from_ptr(ptr: *mut c_char, fallback: &str) -> Result { let message = last_error_message().unwrap_or_else(|| fallback.to_string()); return Err(FlowError::Internal(message)); } - Ok(ptr_to_json(ptr)) + let value = unsafe { CStr::from_ptr(ptr) }.to_string_lossy(); + serde_json::from_str(&value) + .map_err(|error| FlowError::Internal(format!("{fallback}: invalid JSON: {error}"))) } fn ptr_to_opt_string(ptr: *mut c_char) -> Option { diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index b541da082..b917f4042 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -1178,6 +1178,7 @@ pub fn wrap_js_event_sanitize_fn( Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); Box::pin(async move { + let _active_callback = ActiveEventSanitizerCallback::enter(); let event_json = match JsEvent::try_from_event(&event) { Ok(event) => event.into_json(), Err(error) => { diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 2c51cfef8..b17f66f6b 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -854,9 +854,11 @@ pub fn wrap_py_llm_stream_exec_intercept_fn( /// Wrap a Python callable `(LlmRequest, LlmSanitizeRequestContext) -> Optional`. fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequestFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new( move |request: LlmRequest, context: LlmSanitizeRequestContext| { let py_fn = py_fn.clone(); + let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let result = py_fn @@ -868,7 +870,7 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest ), ) .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future(py, result) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) })) .await?; Python::attach(|py| { @@ -893,14 +895,16 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest /// Wrap a Python callable `(LlmRequest) -> Optional[str]` for LLM conditional guardrails. pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new(move |request: LlmRequest| { let py_fn = py_fn.clone(); + let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let result = py_fn .call1(py, (PyLLMRequest { inner: request },)) .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future(py, result) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) })) .await?; Python::attach(|py| { From 4ace258765d9630b45b05886246fb171ea9faf3f Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 21:54:53 -0400 Subject: [PATCH 34/83] fix: resolve async middleware review findings Signed-off-by: Will Killian --- .../adaptive/src/adaptive_hints_intercept.rs | 41 ++-- .../src/api/runtime/subscriber_dispatcher.rs | 14 ++ crates/ffi/nemo_relay.h | 13 +- crates/ffi/src/callable.rs | 58 ++++- crates/ffi/tests/unit/callable_tests.rs | 39 ++-- crates/node/src/api/mod.rs | 28 ++- crates/node/src/callable.rs | 199 ++++-------------- crates/node/src/callback_factory.rs | 83 ++++++-- crates/node/src/promise_call.rs | 60 +++++- crates/node/tests/event_sanitizers_tests.mjs | 85 ++++++++ crates/node/tests/llm_tests.mjs | 27 +++ crates/python/src/py_callable.rs | 69 ++++-- .../coverage/py_callable_coverage_tests.rs | 92 +++++++- python/nemo_relay/_event_sanitizer_context.py | 5 + python/tests/test_llm.py | 35 +++ 15 files changed, 588 insertions(+), 260 deletions(-) diff --git a/crates/adaptive/src/adaptive_hints_intercept.rs b/crates/adaptive/src/adaptive_hints_intercept.rs index 50437e4da..3a95f06ac 100644 --- a/crates/adaptive/src/adaptive_hints_intercept.rs +++ b/crates/adaptive/src/adaptive_hints_intercept.rs @@ -180,28 +180,25 @@ impl AdaptiveHintsIntercept { let this = this.clone(); let scope_path = extract_scope_path(); let manual_ls = read_manual_latency_sensitivity(); - Box::pin(async move { - let scope_depth = scope_path.len(); - let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed); - - let effective_agent_id = this.effective_agent_id(); - let cached_hints = - this.load_hints(&scope_path, &effective_agent_id, call_index, scope_depth); - let final_hints = apply_manual_latency_override( - cached_hints, - manual_ls, - &effective_agent_id, - scope_depth, - ); - - if let Some(hints) = final_hints { - inject_agent_hints(&mut request, &mut annotated, &hints); - } - - Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new( - request, annotated, - )) - }) + let scope_depth = scope_path.len(); + let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed); + let effective_agent_id = this.effective_agent_id(); + let cached_hints = + this.load_hints(&scope_path, &effective_agent_id, call_index, scope_depth); + let final_hints = apply_manual_latency_override( + cached_hints, + manual_ls, + &effective_agent_id, + scope_depth, + ); + + if let Some(hints) = final_hints { + inject_agent_hints(&mut request, &mut annotated, &hints); + } + + let outcome = + nemo_relay::api::llm::LlmRequestInterceptOutcome::new(request, annotated); + Box::pin(async move { Ok(outcome) }) }, ) } diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 47dace406..43412585b 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -198,6 +198,10 @@ mod native { Ok(()) } + pub(super) fn in_dispatcher_callback() -> bool { + IN_DISPATCHER.with(Cell::get) + } + fn dispatcher_sender() -> std::result::Result, String> { DISPATCHER.get_or_init(start_dispatcher).clone() } @@ -429,3 +433,13 @@ pub(crate) fn register_async_publication() -> Option pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() } + +/// Return whether the current callback was invoked by queued event publication. +/// +/// Bindings use this to make re-entrant flush operations non-blocking while +/// the serial dispatcher is awaiting middleware on another language runtime. +#[doc(hidden)] +#[must_use] +pub fn in_dispatcher_callback() -> bool { + native::in_dispatcher_callback() +} diff --git a/crates/ffi/nemo_relay.h b/crates/ffi/nemo_relay.h index 70c480b24..accaa5e9e 100644 --- a/crates/ffi/nemo_relay.h +++ b/crates/ffi/nemo_relay.h @@ -252,6 +252,10 @@ typedef char *(*NemoRelayEventSanitizeCb)(void *user_data, /** * Optional destructor for user data passed to callbacks. * Called when the runtime no longer needs the associated callback. + * + * Middleware callbacks may run concurrently on Relay runtime or publication + * threads. Callers must keep `user_data` valid and thread-safe until this + * destructor runs. */ typedef void (*NemoRelayFreeFn)(void *user_data); @@ -361,6 +365,10 @@ typedef NemoRelayStatus (*NemoRelayLlmRequestInterceptCb)(void *user_data, /** * Runtime-provided "next" callback for LLM execution middleware chain. * Takes a native JSON C string, returns a response JSON C string. + * `next_ctx` is borrowed and valid only until the intercept callback returns; + * callers must not retain it or invoke `next_fn` asynchronously. The returned + * string belongs to the caller and must be released with + * `nemo_relay_string_free`. */ typedef char *(*NemoRelayLlmExecNextFn)(const char *native_json, void *next_ctx); @@ -405,7 +413,10 @@ typedef char *(*NemoRelayToolConditionalCb)(void *user_data, const char *name, c /** * Runtime-provided "next" callback for tool execution middleware chain. * Call this from an intercept to invoke the next layer (or original function). - * `next_ctx` is an opaque pointer managed by the runtime. + * `next_ctx` is borrowed and valid only until the intercept callback returns; + * callers must not retain it or invoke `next_fn` asynchronously. The returned + * string belongs to the caller and must be released with + * `nemo_relay_string_free`. */ typedef char *(*NemoRelayToolExecNextFn)(const char *args_json, void *next_ctx); diff --git a/crates/ffi/src/callable.rs b/crates/ffi/src/callable.rs index 58d4c3f60..054c098bd 100644 --- a/crates/ffi/src/callable.rs +++ b/crates/ffi/src/callable.rs @@ -38,7 +38,7 @@ use nemo_relay::codec::request::AnnotatedLlmRequest as AnnotatedLLMRequest; use nemo_relay::codec::traits::LlmCodec; use nemo_relay::error::{FlowError, Result}; -use crate::convert::{c_str_to_json, json_to_c_string}; +use crate::convert::json_to_c_string; use crate::error::{NemoRelayStatus, clear_last_error, last_error_message, set_last_error}; use crate::types::{FfiEvent, FfiLLMRequest, FfiPluginContext}; @@ -48,6 +48,10 @@ use crate::types::{FfiEvent, FfiLLMRequest, FfiPluginContext}; /// Optional destructor for user data passed to callbacks. /// Called when the runtime no longer needs the associated callback. +/// +/// Middleware callbacks may run concurrently on Relay runtime or publication +/// threads. Callers must keep `user_data` valid and thread-safe until this +/// destructor runs. pub type NemoRelayFreeFn = Option; /// Callback for tool request/response sanitization guardrails and intercepts. @@ -76,7 +80,10 @@ pub type NemoRelayToolExecCb = /// Runtime-provided "next" callback for tool execution middleware chain. /// Call this from an intercept to invoke the next layer (or original function). -/// `next_ctx` is an opaque pointer managed by the runtime. +/// `next_ctx` is borrowed and valid only until the intercept callback returns; +/// callers must not retain it or invoke `next_fn` asynchronously. The returned +/// string belongs to the caller and must be released with +/// `nemo_relay_string_free`. pub type NemoRelayToolExecNextFn = unsafe extern "C" fn(args_json: *const c_char, next_ctx: *mut libc::c_void) -> *mut c_char; @@ -169,6 +176,10 @@ pub type NemoRelayLlmExecCb = /// Runtime-provided "next" callback for LLM execution middleware chain. /// Takes a native JSON C string, returns a response JSON C string. +/// `next_ctx` is borrowed and valid only until the intercept callback returns; +/// callers must not retain it or invoke `next_fn` asynchronously. The returned +/// string belongs to the caller and must be released with +/// `nemo_relay_string_free`. pub type NemoRelayLlmExecNextFn = unsafe extern "C" fn(native_json: *const c_char, next_ctx: *mut libc::c_void) -> *mut c_char; @@ -398,6 +409,7 @@ pub fn wrap_tool_exec_fn( Box::new(move |args: Json| { let ud = ud.clone(); Box::pin(async move { + clear_last_error(); let c_args = json_to_c_string(&args); let result_ptr = unsafe { cb(ud.ptr, c_args) }; unsafe { nemo_relay_string_free_internal(c_args) }; @@ -454,6 +466,7 @@ pub fn wrap_tool_exec_intercept_fn( } let c_args = json_to_c_string(&args); + clear_last_error(); let result_ptr = unsafe { cb(ud.ptr, c_args, tool_next_trampoline, next_ctx) }; unsafe { drop(Box::from_raw(next_ctx as *mut ToolExecutionNextFn)) }; unsafe { nemo_relay_string_free_internal(c_args) }; @@ -526,6 +539,7 @@ pub fn wrap_llm_exec_intercept_fn( let request_json = serde_json::to_value(&request).unwrap_or(Json::Null); let c_request = json_to_c_string(&request_json); + clear_last_error(); let result_ptr = unsafe { cb(ud.ptr, c_request, llm_next_trampoline, next_ctx) }; unsafe { drop(Box::from_raw(next_ctx as *mut LlmExecutionNextFn)) }; unsafe { nemo_relay_string_free_internal(c_request) }; @@ -601,6 +615,7 @@ pub fn wrap_llm_stream_exec_intercept_fn( let request_json = serde_json::to_value(&request).unwrap_or(Json::Null); let c_request = json_to_c_string(&request_json); + clear_last_error(); let result_ptr = unsafe { cb(ud.ptr, c_request, llm_stream_next_trampoline, next_ctx) }; unsafe { drop(Box::from_raw(next_ctx as *mut LlmStreamExecutionNextFn)) }; @@ -710,7 +725,7 @@ pub fn wrap_llm_sanitize_request_fn( Ok(identity) => identity, Err(error) => { set_last_error(&error.to_string()); - return Ok(None); + return Err(error); } }; let codec = context @@ -727,7 +742,10 @@ pub fn wrap_llm_sanitize_request_fn( let result_ptr = unsafe { cb(ud.ptr, ffi_req, ffi_context) }; if result_ptr.is_null() { unsafe { drop(Box::from_raw(ffi_req)) }; - return Ok(None); + return match last_error_message() { + Some(message) => Err(FlowError::Internal(message)), + None => Ok(None), + }; } if result_ptr == ffi_req { return Ok(Some(unsafe { Box::from_raw(ffi_req) }.0)); @@ -754,7 +772,7 @@ pub fn wrap_llm_sanitize_response_fn( Ok(identity) => identity, Err(error) => { set_last_error(&error.to_string()); - return Ok(None); + return Err(error); } }; let codec = context @@ -771,16 +789,32 @@ pub fn wrap_llm_sanitize_response_fn( let result_ptr = unsafe { cb(ud.ptr, response_json, ffi_context) }; if result_ptr.is_null() { unsafe { nemo_relay_string_free_internal(response_json) }; - return Ok(None); + return match last_error_message() { + Some(message) => Err(FlowError::Internal(message)), + None => Ok(None), + }; } - let result = c_str_to_json(result_ptr); + let result = unsafe { CStr::from_ptr(result_ptr) } + .to_str() + .map_err(|error| { + FlowError::Internal(format!( + "LLM response sanitizer returned invalid UTF-8: {error}" + )) + }) + .and_then(|value| { + serde_json::from_str(value).map_err(|error| { + FlowError::Internal(format!( + "LLM response sanitizer returned invalid JSON: {error}" + )) + }) + }); unsafe { nemo_relay_string_free_internal(response_json); if result_ptr != response_json { nemo_relay_string_free_internal(result_ptr); } } - Ok(result) + result.map(Some) }) }) } @@ -842,6 +876,7 @@ pub fn wrap_llm_exec_fn( Box::new(move |request: LlmRequest| { let ud = ud.clone(); Box::pin(async move { + clear_last_error(); let request_json = serde_json::to_value(&request).unwrap_or(Json::Null); let c_request = json_to_c_string(&request_json); let result_ptr = unsafe { cb(ud.ptr, c_request) }; @@ -867,6 +902,7 @@ pub fn wrap_llm_stream_exec_fn( Box::new(move |request: LlmRequest| { let ud = ud.clone(); Box::pin(async move { + clear_last_error(); let request_json = serde_json::to_value(&request).unwrap_or(Json::Null); let c_request = json_to_c_string(&request_json); let result_ptr = unsafe { cb(ud.ptr, c_request) }; @@ -1056,8 +1092,10 @@ fn json_result_from_ptr(ptr: *mut c_char, fallback: &str) -> Result { let message = last_error_message().unwrap_or_else(|| fallback.to_string()); return Err(FlowError::Internal(message)); } - let value = unsafe { CStr::from_ptr(ptr) }.to_string_lossy(); - serde_json::from_str(&value) + let value = unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(|error| FlowError::Internal(format!("{fallback}: invalid UTF-8: {error}")))?; + serde_json::from_str(value) .map_err(|error| FlowError::Internal(format!("{fallback}: invalid JSON: {error}"))) } diff --git a/crates/ffi/tests/unit/callable_tests.rs b/crates/ffi/tests/unit/callable_tests.rs index 70469b4f4..4542bb551 100644 --- a/crates/ffi/tests/unit/callable_tests.rs +++ b/crates/ffi/tests/unit/callable_tests.rs @@ -473,32 +473,37 @@ fn test_wrap_llm_request_response_and_conditional_callbacks() { for callback in [invalid_json_cb, invalid_utf8_cb] { let malformed_response = wrap_llm_sanitize_response_fn(callback, std::ptr::null_mut(), None); - assert_eq!( - resolve(malformed_response( - json!({"secret": "must be omitted"}), - nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), - )) - .unwrap(), - None + let error = resolve(malformed_response( + json!({"secret": "must be preserved"}), + nemo_relay::api::runtime::LlmSanitizeResponseContext::default(), + )) + .unwrap_err(); + assert!( + error.to_string().contains("invalid"), + "unexpected sanitizer error: {error}" ); } } #[test] -fn test_llm_sanitizers_fail_closed_for_runtime_codec_ids_with_embedded_nul() { +fn test_llm_sanitizers_report_runtime_codec_ids_with_embedded_nul() { let runtime_identity = nemo_relay::api::runtime::LlmCodecIdentity::Runtime("runtime\0codec".to_string()); let request_sanitizer = wrap_llm_sanitize_request_fn(llm_request_alias_cb, std::ptr::null_mut(), None); - let request_result = resolve(request_sanitizer( + let request_error = resolve(request_sanitizer( make_request(), nemo_relay::api::runtime::LlmSanitizeRequestContext::with_identity( runtime_identity.clone(), ), )) - .expect("legacy sanitizer wrappers report callback errors out of band"); - assert_eq!(request_result, None); + .unwrap_err(); + assert!( + request_error + .to_string() + .contains("runtime codec ID contains an embedded NUL") + ); assert!( last_error_message() .unwrap() @@ -507,12 +512,16 @@ fn test_llm_sanitizers_fail_closed_for_runtime_codec_ids_with_embedded_nul() { let response_sanitizer = wrap_llm_sanitize_response_fn(json_alias_cb, std::ptr::null_mut(), None); - let response_result = resolve(response_sanitizer( - json!({"secret": "must be omitted"}), + let response_error = resolve(response_sanitizer( + json!({"secret": "must be preserved"}), nemo_relay::api::runtime::LlmSanitizeResponseContext::with_identity(runtime_identity), )) - .expect("legacy sanitizer wrappers report callback errors out of band"); - assert_eq!(response_result, None); + .unwrap_err(); + assert!( + response_error + .to_string() + .contains("runtime codec ID contains an embedded NUL") + ); assert!( last_error_message() .unwrap() diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 6e22c3dc9..9c00ee254 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -1413,7 +1413,9 @@ impl PersistentJsFunction { } fn node_event_sanitize_fn(env: &Env, func: &JsFunction) -> napi::Result { - let callback = Arc::new(crate::promise_call::PromiseAwareFn::new(env, func)?); + let callback = Arc::new(crate::promise_call::PromiseAwareFn::new_event_sanitizer( + env, func, + )?); Ok(callable::wrap_js_event_sanitize_promise_fn(callback)) } @@ -3180,15 +3182,21 @@ pub fn deregister_subscriber(name: String) -> Result { /// /// The Promise rejects if the blocking task fails or the core subscriber flush returns an error. /// Callers should handle errors when awaiting it. -#[napi] -pub async fn flush_subscribers() -> Result<()> { - if crate::callable::event_sanitizer_callback_active() { - return Ok(()); - } - tokio::task::spawn_blocking(core_subscriber_api::flush_subscribers) - .await - .map_err(|error| to_napi_err(FlowError::Internal(error.to_string())))? - .map_err(to_napi_err) +#[napi(ts_return_type = "Promise")] +pub fn flush_subscribers(env: Env) -> Result { + let reentrant = crate::callback_factory::event_sanitizer_callback_active(&env)?; + env.execute_tokio_future( + async move { + if reentrant { + return Ok(()); + } + tokio::task::spawn_blocking(core_subscriber_api::flush_subscribers) + .await + .map_err(|error| to_napi_err(FlowError::Internal(error.to_string())))? + .map_err(to_napi_err) + }, + |env, _| env.get_undefined(), + ) } // --------------------------------------------------------------------------- diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index b917f4042..905f77e49 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -12,7 +12,6 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; use napi::bindgen_prelude::ToNapiValue; use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}; @@ -45,27 +44,6 @@ use crate::convert::{callback_json, record_callback_error, to_napi_err}; use crate::promise_call::{JsonNextFn, JsonStreamNextFn, PromiseAwareFn}; use crate::types::{EventSanitizeFields, JsEvent, event_sanitize_fields_from_json}; -static ACTIVE_EVENT_SANITIZER_CALLBACKS: AtomicUsize = AtomicUsize::new(0); - -struct ActiveEventSanitizerCallback; - -impl ActiveEventSanitizerCallback { - fn enter() -> Self { - ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_add(1, Ordering::AcqRel); - Self - } -} - -impl Drop for ActiveEventSanitizerCallback { - fn drop(&mut self) { - ACTIVE_EVENT_SANITIZER_CALLBACKS.fetch_sub(1, Ordering::AcqRel); - } -} - -pub(crate) fn event_sanitizer_callback_active() -> bool { - ACTIVE_EVENT_SANITIZER_CALLBACKS.load(Ordering::Acquire) != 0 -} - /// Structured codec identity delivered to JavaScript LLM sanitizers. #[napi(object)] #[derive(Clone)] @@ -321,6 +299,8 @@ pub fn wrap_js_llm_sanitize_request_promise_fn(func: Arc) -> Llm Arc::new( move |request: LlmRequest, context: LlmSanitizeRequestContext| { let func = func.clone(); + let publication = + nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let request = serde_json::to_value(request).map_err(|error| { let error = FlowError::Internal(format!( @@ -330,26 +310,26 @@ pub fn wrap_js_llm_sanitize_request_promise_fn(func: Arc) -> Llm error })?; let context = js_llm_sanitize_request_context(&context); - let value = func - .call_spread_with_arg0(Box::new(move |env| { - let mut args = env.create_array_with_length(2)?; - let request = unsafe { - JsUnknown::from_raw_unchecked( - env.raw(), - Json::to_napi_value(env.raw(), request)?, - ) - }; - args.set_element(0, request)?; - args.set_element( - 1, - js_llm_sanitize_request_context_to_napi(env, context)?, - )?; - Ok(js_object_to_unknown(env, args)) - })) - .await - .inspect_err(|error| { - record_callback_error(error.to_string()); - })?; + let build_args: crate::promise_call::Arg0Builder = Box::new(move |env| { + let mut args = env.create_array_with_length(2)?; + let request = unsafe { + JsUnknown::from_raw_unchecked( + env.raw(), + Json::to_napi_value(env.raw(), request)?, + ) + }; + args.set_element(0, request)?; + args.set_element(1, js_llm_sanitize_request_context_to_napi(env, context)?)?; + Ok(js_object_to_unknown(env, args)) + }); + let value = if publication { + func.call_spread_with_arg0_for_publication(build_args).await + } else { + func.call_spread_with_arg0(build_args).await + } + .inspect_err(|error| { + record_callback_error(error.to_string()); + })?; if value.is_null() { Ok(None) } else { @@ -374,25 +354,29 @@ pub fn wrap_js_llm_sanitize_response_promise_fn( ) -> LlmSanitizeResponseFn { Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { let func = func.clone(); + let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let context = js_llm_sanitize_response_context(&context); - let value = func - .call_spread_with_arg0(Box::new(move |env| { - let mut args = env.create_array_with_length(2)?; - let response = unsafe { - JsUnknown::from_raw_unchecked( - env.raw(), - Json::to_napi_value(env.raw(), response)?, - ) - }; - args.set_element(0, response)?; - args.set_element(1, js_llm_sanitize_response_context_to_napi(env, context)?)?; - Ok(js_object_to_unknown(env, args)) - })) - .await - .inspect_err(|error| { - record_callback_error(error.to_string()); - })?; + let build_args: crate::promise_call::Arg0Builder = Box::new(move |env| { + let mut args = env.create_array_with_length(2)?; + let response = unsafe { + JsUnknown::from_raw_unchecked( + env.raw(), + Json::to_napi_value(env.raw(), response)?, + ) + }; + args.set_element(0, response)?; + args.set_element(1, js_llm_sanitize_response_context_to_napi(env, context)?)?; + Ok(js_object_to_unknown(env, args)) + }); + let value = if publication { + func.call_spread_with_arg0_for_publication(build_args).await + } else { + func.call_spread_with_arg0(build_args).await + } + .inspect_err(|error| { + record_callback_error(error.to_string()); + })?; Ok((!value.is_null()).then_some(value)) }) }) @@ -498,7 +482,6 @@ pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSani Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); Box::pin(async move { - let _active_callback = ActiveEventSanitizerCallback::enter(); let event_json = JsEvent::try_from_event(&event) .map(JsEvent::into_json) .map_err(|error| { @@ -1170,102 +1153,6 @@ pub fn wrap_js_event_subscriber( }) } -/// Wrap a JS event sanitizer: ``(event, fields) => fields``. -pub fn wrap_js_event_sanitize_fn( - func: ThreadsafeFunction<(Json, Json), ErrorStrategy::Fatal>, -) -> EventSanitizeFn { - let func = Arc::new(func); - Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { - let func = func.clone(); - Box::pin(async move { - let _active_callback = ActiveEventSanitizerCallback::enter(); - let event_json = match JsEvent::try_from_event(&event) { - Ok(event) => event.into_json(), - Err(error) => { - record_callback_error(format!( - "nemo_relay: failed to serialize JS event sanitizer context: {error}" - )); - return Err(FlowError::Internal(error.to_string())); - } - }; - let js_fields = EventSanitizeFields { - data: fields.data, - category_profile: fields - .category_profile - .as_ref() - .map(serde_json::to_value) - .transpose() - .map_err(|error| { - let error = FlowError::Internal(format!( - "failed to serialize JS event sanitizer category profile: {error}" - )); - record_callback_error(error.to_string()); - error - })?, - metadata: fields.metadata, - }; - let js_fields = serde_json::to_value(js_fields).map_err(|error| { - let error = FlowError::Internal(format!( - "failed to serialize JS event sanitizer fields: {error}" - )); - record_callback_error(error.to_string()); - error - })?; - let (tx, rx) = tokio::sync::oneshot::channel(); - let status = func.call_with_return_value( - (event_json, js_fields), - ThreadsafeFunctionCallMode::Blocking, - move |value: Option| { - let _ = tx.send(callback_json(value)); - Ok(()) - }, - ); - if status != napi::Status::Ok { - record_callback_error(format!( - "nemo_relay: failed to queue JS event sanitizer callback: {status:?}" - )); - return Err(FlowError::Internal(format!( - "failed to queue JS event sanitizer callback: {status:?}" - ))); - } - let sanitized: Result = async { - let result = await_middleware_json_result( - rx, - "nemo_relay: JS event sanitizer callback failed", - ) - .await?; - let result = event_sanitize_fields_from_json(result).map_err(|error| { - FlowError::Internal(format!( - "nemo_relay: invalid JS event sanitizer result: {error}" - )) - })?; - let category_profile = result - .category_profile - .map(serde_json::from_value) - .transpose() - .map_err(|error| { - FlowError::Internal(format!( - "nemo_relay: invalid JS event sanitizer result: {error}" - )) - })?; - Ok(CoreEventSanitizeFields { - data: result.data, - category_profile, - metadata: result.metadata, - }) - } - .await; - match sanitized { - Ok(sanitized) => Ok(sanitized), - Err(error) => { - record_callback_error(error.to_string()); - Err(error) - } - } - }) - }) -} - // --------------------------------------------------------------------------- // Codec wrappers // --------------------------------------------------------------------------- diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index 5fcd45ed8..9314c544c 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -5,9 +5,12 @@ use napi::{Env, JsFunction, JsObject, JsUnknown, NapiRaw, NapiValue}; -const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v1"; +const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v2"; const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { + const { AsyncLocalStorage } = process.getBuiltinModule('node:async_hooks'); + const eventSanitizerContext = new AsyncLocalStorage(); + function jsonValue(value, seen = new Set()) { if (value === null || typeof value === 'string' || typeof value === 'boolean') { return value; @@ -49,6 +52,38 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { return result; } + function callPromise(fn, arg0, spread, next, resolve, reject, publication) { + const token = { active: publication }; + const invoke = () => { + Promise.resolve().then(() => ( + next === undefined + ? (spread ? fn(...arg0) : fn(arg0)) + : (spread ? fn(...arg0, next) : fn(arg0, next)) + )).then((value) => jsonValue(value === undefined ? null : value)).then((value) => { + token.active = false; + resolve(value); + }, (error) => { + token.active = false; + let message = 'unknown error'; + try { + if (typeof error === 'string') { + message = error; + } else if (error === null || (typeof error !== 'object' && typeof error !== 'function')) { + message = String(error); + } else if (error != null && typeof error.message === 'string') { + message = error.message; + } + } catch {} + reject(message); + }); + }; + if (publication) { + eventSanitizerContext.run(token, invoke); + } else { + invoke(); + } + } + return { execution(fn) { return function __nemo_relay_execution_wrapper(...args) { @@ -66,7 +101,7 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { }, promise(fn) { - return function __nemo_relay_promise_wrapper(error, arg0, spread, next, resolve, reject) { + return function __nemo_relay_promise_wrapper(error, arg0, spread, next, resolve, reject, publication) { if (error != null) { let message = 'unknown error'; try { @@ -75,25 +110,27 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { reject(message); return; } - Promise.resolve().then(() => ( - next === undefined - ? (spread ? fn(...arg0) : fn(arg0)) - : (spread ? fn(...arg0, next) : fn(arg0, next)) - )).then((value) => jsonValue(value === undefined ? null : value)).then(resolve, (error) => { + callPromise(fn, arg0, spread, next, resolve, reject, publication); + }; + }, + + eventSanitizerPromise(fn) { + return function __nemo_relay_event_sanitizer_promise_wrapper(error, arg0, spread, next, resolve, reject) { + if (error != null) { let message = 'unknown error'; try { - if (typeof error === 'string') { - message = error; - } else if (error === null || (typeof error !== 'object' && typeof error !== 'function')) { - message = String(error); - } else if (error != null && typeof error.message === 'string') { - message = error.message; - } + message = String(error?.message ?? error); } catch {} reject(message); - }); + return; + } + callPromise(fn, arg0, spread, next, resolve, reject, true); }; }, + + eventSanitizerCallbackActive() { + return eventSanitizerContext.getStore()?.active === true; + }, }; })()"#; @@ -140,3 +177,19 @@ pub(crate) fn wrap_execution_callback(env: &Env, func: &JsFunction) -> napi::Res pub(crate) fn wrap_promise_callback(env: &Env, func: &JsFunction) -> napi::Result { wrap_callback(env, func, "promise") } + +pub(crate) fn wrap_event_sanitizer_callback( + env: &Env, + func: &JsFunction, +) -> napi::Result { + wrap_callback(env, func, "eventSanitizerPromise") +} + +pub(crate) fn event_sanitizer_callback_active(env: &Env) -> napi::Result { + let factories = callback_factories(env)?; + let callback: JsFunction = factories.get_named_property("eventSanitizerCallbackActive")?; + callback + .call::(None, &[])? + .coerce_to_bool()? + .get_value() +} diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index cdc15bada..df9a7785a 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -55,6 +55,7 @@ struct CallArgs { arg0: PrimaryArg, spread: bool, next: Option, + publication: bool, completion: CallCompletion, } @@ -184,8 +185,19 @@ impl PromiseAwareFn { /// Must be called on the JS main thread (i.e., in a sync `#[napi]` function). pub fn new(env: &Env, func: &JsFunction) -> napi::Result { let wrapper = callback_factory::wrap_promise_callback(env, func)?; + Self::from_wrapper(env, &wrapper) + } + + /// Create a callback wrapper that marks only its JavaScript async context + /// as an active event sanitizer. + pub fn new_event_sanitizer(env: &Env, func: &JsFunction) -> napi::Result { + let wrapper = callback_factory::wrap_event_sanitizer_callback(env, func)?; + Self::from_wrapper(env, &wrapper) + } + + fn from_wrapper(env: &Env, wrapper: &JsFunction) -> napi::Result { let mut tsfn = - env.create_threadsafe_function(&wrapper, 0, |ctx: ThreadSafeCallContext| { + env.create_threadsafe_function(wrapper, 0, |ctx: ThreadSafeCallContext| { let next = match ctx.value.next { Some(next) => build_next_unknown(&ctx.env, next)?, None => undefined_to_unknown(&ctx.env)?, @@ -202,7 +214,13 @@ impl PromiseAwareFn { ctx.env.get_boolean(ctx.value.spread)?.raw(), ) }; - let args = vec![arg0, spread, next, resolve, reject]; + let publication = unsafe { + JsUnknown::from_raw_unchecked( + ctx.env.raw(), + ctx.env.get_boolean(ctx.value.publication)?.raw(), + ) + }; + let args = vec![arg0, spread, next, resolve, reject, publication]; Ok(args) })?; @@ -216,7 +234,8 @@ impl PromiseAwareFn { /// Call the JS function with the given args and await the result. pub async fn call(&self, args: Json) -> FlowResult { - self.call_inner(PrimaryArg::Json(args), false, None).await + self.call_inner(PrimaryArg::Json(args), false, None, false) + .await } /// Call a JavaScript callback with several JSON arguments. @@ -225,7 +244,7 @@ impl PromiseAwareFn { /// guardrails, whose public contract is `(name, payload)` rather than a /// single envelope object. pub async fn call_spread(&self, args: Vec) -> FlowResult { - self.call_inner(PrimaryArg::Json(Json::Array(args)), true, None) + self.call_inner(PrimaryArg::Json(Json::Array(args)), true, None, false) .await } @@ -236,21 +255,35 @@ impl PromiseAwareFn { /// cannot cross the threadsafe-function boundary as plain JSON, such as a /// `#[napi]` class instance. pub async fn call_with_arg0(&self, build_arg0: Arg0Builder) -> FlowResult { - self.call_inner(PrimaryArg::Build(build_arg0), false, None) + self.call_inner(PrimaryArg::Build(build_arg0), false, None, false) .await } /// Call a JavaScript callback with builder-constructed spread arguments. pub async fn call_spread_with_arg0(&self, build_arg0: Arg0Builder) -> FlowResult { - self.call_inner(PrimaryArg::Build(build_arg0), true, None) + self.call_inner(PrimaryArg::Build(build_arg0), true, None, false) + .await + } + + /// Call a spread callback from queued event publication. + pub async fn call_spread_with_arg0_for_publication( + &self, + build_arg0: Arg0Builder, + ) -> FlowResult { + self.call_inner(PrimaryArg::Build(build_arg0), true, None, true) .await } /// Call the JS function with a middleware-style `next(arg)` callback that /// resolves to a JSON result. pub async fn call_with_json_next(&self, args: Json, next: JsonNextFn) -> FlowResult { - self.call_inner(PrimaryArg::Json(args), false, Some(NextFn::Json(next))) - .await + self.call_inner( + PrimaryArg::Json(args), + false, + Some(NextFn::Json(next)), + false, + ) + .await } /// Call the JS function with a middleware-style `next(arg)` callback that @@ -260,8 +293,13 @@ impl PromiseAwareFn { args: Json, next: JsonStreamNextFn, ) -> FlowResult { - self.call_inner(PrimaryArg::Json(args), false, Some(NextFn::Stream(next))) - .await + self.call_inner( + PrimaryArg::Json(args), + false, + Some(NextFn::Stream(next)), + false, + ) + .await } /// Release the underlying threadsafe function so it does not outlive its registration. @@ -276,6 +314,7 @@ impl PromiseAwareFn { arg0: PrimaryArg, spread: bool, next: Option, + publication: bool, ) -> FlowResult { let (sender, receiver) = tokio::sync::oneshot::channel(); let tsfn = self @@ -290,6 +329,7 @@ impl PromiseAwareFn { arg0, spread, next, + publication, completion: CallCompletion::new(sender), }), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index e437d9574..2380f2cbc 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -148,6 +148,91 @@ describe('event sanitizer registries', () => { assert.equal(flushReturned, true); }); + it('does not treat an unrelated flush as sanitizer re-entrancy', async () => { + const events = capture('node-event-sanitize-independent-flush-sub'); + let releaseSanitizer; + let sanitizerEntered; + const entered = new Promise((resolve) => { + sanitizerEntered = resolve; + }); + const release = new Promise((resolve) => { + releaseSanitizer = resolve; + }); + lib.registerMarkSanitizeGuardrail('node-event-independent-flush', 0, async (_event, fields) => { + sanitizerEntered(); + await release; + return fields; + }); + try { + lib.event('independent-flush-checkpoint', null, { raw: true }); + await entered; + const flush = lib.flushSubscribers(); + const state = await Promise.race([ + flush.then(() => 'flushed'), + new Promise((resolve) => setImmediate(() => resolve('pending'))), + ]); + assert.equal(state, 'pending'); + releaseSanitizer(); + await flush; + await waitFor(events, 1); + } finally { + releaseSanitizer(); + lib.deregisterMarkSanitizeGuardrail('node-event-independent-flush'); + lib.deregisterSubscriber('node-event-sanitize-independent-flush-sub'); + } + }); + + it('clears sanitizer re-entrancy in async descendants after settlement', async () => { + const events = capture('node-event-sanitize-descendant-flush-sub'); + let secondSanitizerEntered; + const secondEntered = new Promise((resolve) => { + secondSanitizerEntered = resolve; + }); + let releaseSecondSanitizer; + const releaseSecond = new Promise((resolve) => { + releaseSecondSanitizer = resolve; + }); + let descendantFlushStarted; + const flushStarted = new Promise((resolve) => { + descendantFlushStarted = resolve; + }); + let descendantFlush; + const flushed = new Promise((resolve, reject) => { + descendantFlush = { resolve, reject }; + }); + lib.registerMarkSanitizeGuardrail('node-event-descendant-flush', 0, async (event, fields) => { + if (event.name === 'descendant-flush-origin') { + setTimeout(async () => { + await secondEntered; + descendantFlushStarted(); + lib.flushSubscribers().then(descendantFlush.resolve, descendantFlush.reject); + }, 0); + } else if (event.name === 'descendant-flush-blocked') { + secondSanitizerEntered(); + await releaseSecond; + } + return fields; + }); + try { + lib.event('descendant-flush-origin', null, { raw: true }); + lib.event('descendant-flush-blocked', null, { raw: true }); + await secondEntered; + await flushStarted; + const state = await Promise.race([ + flushed.then(() => 'flushed'), + new Promise((resolve) => setImmediate(() => resolve('pending'))), + ]); + assert.equal(state, 'pending'); + releaseSecondSanitizer(); + await flushed; + await waitFor(events, 2); + } finally { + releaseSecondSanitizer(); + lib.deregisterMarkSanitizeGuardrail('node-event-descendant-flush'); + lib.deregisterSubscriber('node-event-sanitize-descendant-flush-sub'); + } + }); + it('fails open and records invalid sanitizer results', async () => { const events = capture('node-event-sanitize-invalid-sub'); const invalidResults = { diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index 5408f6fa9..b54939a2d 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -645,6 +645,33 @@ describe('LLM guardrails', () => { deregisterLlmSanitizeRequestGuardrail('node_llm_san_req'); }); + it('manual async sanitizers can flush subscribers without deadlocking', async () => { + let requestFlushed = false; + let responseFlushed = false; + registerSubscriber('node_manual_flush_subscriber', () => {}); + registerLlmSanitizeRequestGuardrail('node_manual_flush_request', 10, async (request) => { + await flushSubscribers(); + requestFlushed = true; + return request; + }); + registerLlmSanitizeResponseGuardrail('node_manual_flush_response', 10, async (response) => { + await flushSubscribers(); + responseFlushed = true; + return response; + }); + try { + const handle = llmCall('node_manual_flush', makeNative()); + llmCallEnd(handle, { response: 'ok' }); + await flushSubscribers(); + } finally { + deregisterLlmSanitizeRequestGuardrail('node_manual_flush_request'); + deregisterLlmSanitizeResponseGuardrail('node_manual_flush_response'); + deregisterSubscriber('node_manual_flush_subscriber'); + } + assert.equal(requestFlushed, true); + assert.equal(responseFlushed, true); + }); + it('sanitize request guardrail rewrites start event payload', async () => { const events = []; registerSubscriber('node_llm_san_req_evt', (e) => events.push(e)); diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index b17f66f6b..ce23c5ec3 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -32,6 +32,7 @@ use nemo_relay::api::runtime::{ ToolConditionalFn, ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn, }; use nemo_relay::error::{FlowError, Result as FlowResult}; +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use pyo3::types::PyDict; use pyo3_async_runtimes::TaskLocals; @@ -128,7 +129,15 @@ fn split_py_object_or_future( py: Python<'_>, result: Py, ) -> FlowResult, PyValueFuture>> { - split_py_object_or_future_with_locals(py, result, None) + let bound = result.bind(py); + if bound.getattr("__await__").is_ok() { + reject_awaitable_from_sync_caller(bound)?; + let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) + .map_err(|error| FlowError::Internal(error.to_string()))?; + Ok(Err(Box::pin(future))) + } else { + Ok(Ok(result)) + } } fn split_py_object_or_future_with_locals( @@ -144,10 +153,21 @@ fn split_py_object_or_future_with_locals( pyo3_async_runtimes::into_future_with_locals(locals, result.into_bound(py)) .map_err(|e| FlowError::Internal(e.to_string()))?, ), - None => Box::pin( - pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) - .map_err(|e| FlowError::Internal(e.to_string()))?, - ), + None => Box::pin(async move { + tokio::task::spawn_blocking(move || { + Python::attach(|py| { + let coroutine = py + .import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("await_result")) + .and_then(|await_result| await_result.call1((result.bind(py),)))?; + py.import("asyncio") + .and_then(|asyncio| asyncio.call_method1("run", (coroutine,))) + .map(Bound::unbind) + }) + }) + .await + .map_err(|error| PyRuntimeError::new_err(error.to_string()))? + }), }; Ok(Err(future)) } else { @@ -859,18 +879,23 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest move |request: LlmRequest, context: LlmSanitizeRequestContext| { let py_fn = py_fn.clone(); let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let publication = + nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { - let result = py_fn - .call1( - py, - ( - PyLLMRequest { inner: request }, - PyLlmSanitizeRequestContext { inner: context }, - ), - ) - .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) + let args = ( + PyLLMRequest { inner: request }, + PyLlmSanitizeRequestContext { inner: context }, + ); + let result = if publication { + py.import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("invoke")) + .and_then(|invoke| invoke.call1((py_fn.bind(py), args.0, args.1))) + } else { + py_fn.bind(py).call1(args) + } + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) })) .await?; Python::attach(|py| { @@ -1077,15 +1102,21 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { let py_fn = py_fn.clone(); let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let py_context = PyLlmSanitizeResponseContext { inner: context }; let py_response = json_to_py(py, &response) .map_err(|error| FlowError::Internal(error.to_string()))?; - let result = py_fn - .call1(py, (py_response, py_context)) - .map_err(|error| FlowError::Internal(error.to_string()))?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) + let result = if publication { + py.import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("invoke")) + .and_then(|invoke| invoke.call1((py_fn.bind(py), py_response, py_context))) + } else { + py_fn.bind(py).call1((py_response, py_context)) + } + .map_err(|error| FlowError::Internal(error.to_string()))?; + split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) })) .await?; Python::attach(|py| { diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index ccb6cef65..d8ece09e7 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -18,7 +18,41 @@ fn load_module<'py>(py: Python<'py>, code: &str) -> Bound<'py, PyModule> { PyModule::from_code(py, &code, &file_name, &module_name).unwrap() } -fn install_event_sanitizer_context_module(py: Python<'_>) { +struct InstalledContextModule { + previous_parent: Option>, + previous_context: Option>, +} + +impl Drop for InstalledContextModule { + fn drop(&mut self) { + Python::attach(|py| { + let Ok(modules) = py.import("sys").and_then(|sys| sys.getattr("modules")) else { + return; + }; + let Ok(modules) = modules.cast_into::() else { + return; + }; + for (name, previous) in [ + ("nemo_relay", self.previous_parent.take()), + ( + "nemo_relay._event_sanitizer_context", + self.previous_context.take(), + ), + ] { + match previous { + Some(module) => { + let _ = modules.set_item(name, module); + } + None => { + let _ = modules.del_item(name); + } + } + } + }); + } +} + +fn install_event_sanitizer_context_module(py: Python<'_>) -> InstalledContextModule { let code = CString::new(include_str!( "../../../../python/nemo_relay/_event_sanitizer_context.py" )) @@ -40,10 +74,19 @@ fn install_event_sanitizer_context_module(py: Python<'_>) { .unwrap() .cast_into::() .unwrap(); + let previous_parent = modules.get_item("nemo_relay").unwrap().map(Bound::unbind); + let previous_context = modules + .get_item("nemo_relay._event_sanitizer_context") + .unwrap() + .map(Bound::unbind); modules.set_item("nemo_relay", parent).unwrap(); modules .set_item("nemo_relay._event_sanitizer_context", context) .unwrap(); + InstalledContextModule { + previous_parent, + previous_context, + } } fn make_request() -> LlmRequest { @@ -701,16 +744,23 @@ fn event_sanitize_wrapper_covers_conversion_success_and_error_propagation() { let _python = crate::test_support::init_python_test(); Python::attach(|py| { - install_event_sanitizer_context_module(py); + let _context_module = install_event_sanitizer_context_module(py); let module = load_module( py, r#" +import asyncio + def sanitize(event, fields): assert event.kind == "mark" fields["data"] = {"safe": event.name} fields["metadata"] = None return fields +async def async_sanitize(event, fields): + await asyncio.sleep(0) + fields["data"] = {"async_safe": event.name} + return fields + def raises(event, fields): raise RuntimeError("sanitize boom") @@ -738,6 +788,16 @@ def invalid(event, fields): assert_eq!(sanitized.data, Some(json!({"safe": "checkpoint"}))); assert_eq!(sanitized.metadata, None); + let async_sanitized = runtime + .block_on(wrap_py_event_sanitize_fn( + module.getattr("async_sanitize").unwrap().unbind(), + )(Arc::new(event.clone()), fields.clone())) + .unwrap(); + assert_eq!( + async_sanitized.data, + Some(json!({"async_safe": "checkpoint"})) + ); + let raised = runtime .block_on(wrap_py_event_sanitize_fn( module.getattr("raises").unwrap().unbind(), @@ -810,3 +870,31 @@ async def llm_fail(request): }); }); } + +#[test] +fn background_middleware_accepts_custom_awaitables() { + let _python = crate::test_support::init_python_test(); + let (_context_module, llm_custom) = Python::attach(|py| { + let context_module = install_event_sanitizer_context_module(py); + let module = load_module( + py, + r#" +class CustomAwaitable: + def __await__(self): + async def resolve(): + return None + return resolve().__await__() + +def llm_custom_awaitable(request): + return CustomAwaitable() +"#, + ); + ( + context_module, + wrap_py_llm_conditional_fn(module.getattr("llm_custom_awaitable").unwrap().unbind()), + ) + }); + + let runtime = tokio::runtime::Runtime::new().unwrap(); + assert_eq!(runtime.block_on(llm_custom(make_request())).unwrap(), None); +} diff --git a/python/nemo_relay/_event_sanitizer_context.py b/python/nemo_relay/_event_sanitizer_context.py index f2cc89a09..7991ebf4a 100644 --- a/python/nemo_relay/_event_sanitizer_context.py +++ b/python/nemo_relay/_event_sanitizer_context.py @@ -26,6 +26,11 @@ async def _await_result(result: Awaitable[Any]) -> Any: _ACTIVE.reset(token) +async def await_result(result: Awaitable[Any]) -> Any: + """Await an arbitrary awaitable without changing sanitizer context.""" + return await result + + def invoke(callback: Callable[..., Any], *args: Any) -> Any: """Invoke a sanitizer while marking its sync and async execution contexts.""" token = _ACTIVE.set(True) diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index b956d8f64..22bc1aa18 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -179,6 +179,41 @@ def sanitize_response(response, context): assert context.codec.kind == "none" assert context.codec.id is None + async def test_manual_async_sanitizers_can_flush_subscribers(self): + request_flushed = False + response_flushed = False + + async def sanitize_request(request, context): + nonlocal request_flushed + del context + await asyncio.sleep(0) + subscribers.flush() + request_flushed = True + return request + + async def sanitize_response(response, context): + nonlocal response_flushed + del context + await asyncio.sleep(0) + subscribers.flush() + response_flushed = True + return response + + guardrails.register_llm_sanitize_request("py_manual_flush_request", 1, sanitize_request) + guardrails.register_llm_sanitize_response("py_manual_flush_response", 1, sanitize_response) + subscribers.register("py_manual_flush_subscriber", lambda _event: None) + try: + handle = llm.call("py_manual_flush", make_request()) + llm.call_end(handle, {"response": "ok"}) + await asyncio.wait_for(asyncio.to_thread(subscribers.flush), timeout=2) + finally: + guardrails.deregister_llm_sanitize_request("py_manual_flush_request") + guardrails.deregister_llm_sanitize_response("py_manual_flush_response") + subscribers.deregister("py_manual_flush_subscriber") + + assert request_flushed + assert response_flushed + async def test_sanitizers_resolve_active_builtin_codecs(self): request_codec_used = False response_codec_used = False From 24b02b39bd276fd18d5f2fe72ac7c5543b4e6667 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 22:00:25 -0400 Subject: [PATCH 35/83] test(go): encode streaming fixtures as valid JSON Signed-off-by: Will Killian --- go/nemo_relay/llm/llm_shorthand_test.go | 4 ++-- go/nemo_relay/llm_test.go | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/go/nemo_relay/llm/llm_shorthand_test.go b/go/nemo_relay/llm/llm_shorthand_test.go index 40e3604bc..c8a5aa640 100644 --- a/go/nemo_relay/llm/llm_shorthand_test.go +++ b/go/nemo_relay/llm/llm_shorthand_test.go @@ -6,7 +6,6 @@ package llm_test import ( "encoding/json" "io" - "strings" "testing" "github.com/NVIDIA/NeMo-Relay/go/nemo_relay" @@ -121,7 +120,8 @@ func TestLlmShorthands(t *testing.T) { stream, err := llmpkg.StreamExecute("llm_stream", makeRequest(), func(nativeJSON json.RawMessage) (json.RawMessage, error) { - return json.RawMessage(`"` + strings.ReplaceAll("data: {\"chunk\": 1}\n\ndata: [DONE]\n\n", `"`, `\"`) + `"`), nil + encoded, err := json.Marshal("data: {\"chunk\": 1}\n\ndata: [DONE]\n\n") + return json.RawMessage(encoded), err }, nil, nil, ) diff --git a/go/nemo_relay/llm_test.go b/go/nemo_relay/llm_test.go index bf2a60fe4..5aa1eb837 100644 --- a/go/nemo_relay/llm_test.go +++ b/go/nemo_relay/llm_test.go @@ -1098,7 +1098,8 @@ func TestLlmStreamCallExecuteBasic(t *testing.T) { chunks := `data: {"chunk": 1}` + "\n\n" + `data: {"chunk": 2}` + "\n\n" + `data: [DONE]` + "\n\n" - return json.RawMessage(`"` + strings.ReplaceAll(chunks, `"`, `\"`) + `"`), nil + encoded, err := json.Marshal(chunks) + return json.RawMessage(encoded), err }, nil, nil, ) @@ -1146,7 +1147,8 @@ func TestLlmStreamCallExecuteWithCollectorFinalizer(t *testing.T) { func(nativeJSON json.RawMessage) (json.RawMessage, error) { chunks := `data: {"token": "hello"}` + "\n\n" + `data: [DONE]` + "\n\n" - return json.RawMessage(`"` + strings.ReplaceAll(chunks, `"`, `\"`) + `"`), nil + encoded, err := json.Marshal(chunks) + return json.RawMessage(encoded), err }, collector, finalizer, ) @@ -1412,7 +1414,8 @@ func TestLlmStreamCloseFinalizesPartialResponse(t *testing.T) { chunks := `data: {"chunk": 1}` + "\n\n" + `data: {"chunk": 2}` + "\n\n" + `data: [DONE]` + "\n\n" - return json.RawMessage(`"` + strings.ReplaceAll(chunks, `"`, `\"`) + `"`), nil + encoded, err := json.Marshal(chunks) + return json.RawMessage(encoded), err }, nil, func() string { finalizerCalls++ From c32b5df72ec50b28fda74913f9baefc1db9d1e44 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 22:41:03 -0400 Subject: [PATCH 36/83] fix: prevent queued sanitizer flush deadlocks Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 6 +- .../src/api/runtime/subscriber_dispatcher.rs | 73 +++++++++++++++++-- crates/core/src/logging/rotation.rs | 4 - crates/core/src/stream.rs | 1 + .../tests/coverage/logging_rotation_tests.rs | 34 --------- .../core/tests/coverage/logging_sink_tests.rs | 67 +---------------- .../core/tests/integration/pipeline_tests.rs | 42 +++++++++++ crates/node/src/api/mod.rs | 19 +++-- crates/node/src/callable.rs | 15 ++-- crates/node/src/promise_call.rs | 6 ++ crates/node/tests/llm_tests.mjs | 34 +++++++++ crates/node/tests/tools_tests.mjs | 34 +++++++++ crates/python/src/py_callable.rs | 24 ++++-- docs/about-nemo-relay/concepts/middleware.mdx | 20 +++-- .../advanced-guide.mdx | 20 +++-- docs/reference/event-sanitizers.mdx | 8 +- docs/reference/migration-guides.mdx | 19 +++-- python/nemo_relay/subscribers.py | 6 +- python/tests/test_tools.py | 43 +++++++++++ 19 files changed, 315 insertions(+), 160 deletions(-) delete mode 100644 crates/core/tests/coverage/logging_rotation_tests.rs diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index b6a7ffa52..1472cf66d 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -888,11 +888,13 @@ async fn build_llm_end_payload( /// the handle start time if the current time is not later. /// /// # Returns -/// A [`Result`] that is `Ok(())` when the end event has been emitted. +/// A [`Result`] that is `Ok(())` when the end event has been queued for +/// sanitization and publication. /// /// # Errors /// Returns an error when the runtime owner check fails, internal state cannot be -/// read safely, or response codec decoding fails. +/// read safely, or the event cannot be queued. Sanitizer and response-codec +/// errors discovered during queued publication are logged and fail open. /// /// # Notes /// Sanitize-response guardrails affect only the emitted end-event payload, not diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 43412585b..651d57848 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -18,6 +18,7 @@ pub(crate) type EventTransformFn = Box< mod native { use std::cell::Cell; + use std::collections::VecDeque; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -55,6 +56,9 @@ mod native { thread_local! { static IN_DISPATCHER: Cell = const { Cell::new(false) }; } + tokio::task_local! { + static IN_ASYNC_PUBLICATION: (); + } struct DispatchGuard; @@ -177,7 +181,7 @@ mod native { } pub(super) fn flush_subscribers() -> Result<()> { - if IN_DISPATCHER.with(Cell::get) { + if in_dispatcher_callback() { return Ok(()); } let Some(sender_result) = DISPATCHER.get() else { @@ -199,7 +203,15 @@ mod native { } pub(super) fn in_dispatcher_callback() -> bool { - IN_DISPATCHER.with(Cell::get) + IN_DISPATCHER.with(Cell::get) || IN_ASYNC_PUBLICATION.try_with(|_| ()).is_ok() + } + + pub(super) async fn with_async_publication_context(future: F) -> F::Output { + if IN_ASYNC_PUBLICATION.try_with(|_| ()).is_ok() { + future.await + } else { + IN_ASYNC_PUBLICATION.scope((), future).await + } } fn dispatcher_sender() -> std::result::Result, String> { @@ -248,10 +260,18 @@ mod native { } fn run_dispatcher(rx: Receiver) { - while let Ok(message) = rx.recv() { + let mut pending = VecDeque::new(); + loop { + let message = match pending.pop_front() { + Some(message) => message, + None => match rx.recv() { + Ok(message) => message, + Err(_) => break, + }, + }; match message { DispatcherMessage::Flush { done } => { - let pending_flushes = drain_pending_messages(&rx); + let pending_flushes = drain_pending_messages(&rx, &mut pending); let _ = done.send(()); for pending in pending_flushes { let _ = pending.send(()); @@ -265,13 +285,17 @@ mod native { } } - fn drain_pending_messages(rx: &Receiver) -> Vec> { + fn drain_pending_messages( + rx: &Receiver, + pending: &mut VecDeque, + ) -> Vec> { let mut pending_flushes = Vec::new(); while let Ok(message) = rx.try_recv() { match message { DispatcherMessage::Flush { done } => pending_flushes.push(done), - DispatcherMessage::Barrier { done } => { - let _ = done.recv(); + message @ DispatcherMessage::Barrier { .. } => { + pending.push_back(message); + break; } message => handle_message(message), } @@ -384,6 +408,33 @@ mod native { } } } + + #[cfg(test)] + mod tests { + use super::*; + use std::sync::Mutex; + use std::time::Duration; + + static TEST_MUTEX: Mutex<()> = Mutex::new(()); + + #[test] + fn flush_does_not_wait_for_a_later_publication_barrier() { + let _lock = TEST_MUTEX.lock().unwrap(); + let first = register_async_publication().expect("first publication barrier"); + let sender = dispatcher_sender().expect("dispatcher sender"); + let (flush_tx, flush_rx) = mpsc::channel(); + sender + .send(DispatcherMessage::Flush { done: flush_tx }) + .unwrap(); + let later = register_async_publication().expect("later publication barrier"); + + first.send(()).unwrap(); + flush_rx + .recv_timeout(Duration::from_secs(1)) + .expect("flush queued before the later barrier must complete"); + later.send(()).unwrap(); + } + } } #[cfg(test)] @@ -429,6 +480,14 @@ pub(crate) fn register_async_publication() -> Option native::register_async_publication() } +/// Run asynchronous middleware as part of an already-registered publication. +/// +/// Re-entrant subscriber flushes are no-ops in this context because the +/// publication's FIFO barrier cannot complete until the middleware returns. +pub(crate) async fn with_async_publication_context(future: F) -> F::Output { + native::with_async_publication_context(future).await +} + /// Wait for all queued subscriber callbacks submitted before this call. pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() diff --git a/crates/core/src/logging/rotation.rs b/crates/core/src/logging/rotation.rs index fa55780bb..3314377c1 100644 --- a/crates/core/src/logging/rotation.rs +++ b/crates/core/src/logging/rotation.rs @@ -146,7 +146,3 @@ pub(crate) fn rotated_log_path(base_path: &Path, index: usize) -> PathBuf { } base_path.with_file_name(file_name) } - -#[cfg(test)] -#[path = "../../tests/coverage/logging_rotation_tests.rs"] -mod tests; diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index a7cd81761..feaaace1e 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -337,6 +337,7 @@ impl LlmStreamWrapper { let _ = done.send(()); } }; + let finalize = subscriber_dispatcher::with_async_publication_context(finalize); if background_thread { // `Drop` can run while the current-thread Tokio executor is // synchronously flushing subscribers. Use a dedicated runtime so diff --git a/crates/core/tests/coverage/logging_rotation_tests.rs b/crates/core/tests/coverage/logging_rotation_tests.rs deleted file mode 100644 index 842fa7927..000000000 --- a/crates/core/tests/coverage/logging_rotation_tests.rs +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -use super::*; - -#[test] -fn rotating_writer_rotates_retains_and_reports_closed_file_errors() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("nested").join("relay.log"); - let mut writer = SizeRotatingFileWriter::new(path.clone(), 4, 2).unwrap(); - assert_eq!(writer.write(b"abcd").unwrap(), 4); - writer.flush().unwrap(); - assert_eq!(writer.write(b"e").unwrap(), 1); - writer.flush().unwrap(); - - assert_eq!(std::fs::read(rotated_log_path(&path, 1)).unwrap(), b"abcd"); - assert_eq!(std::fs::read(&path).unwrap(), b"e"); - - writer.file = None; - assert!(writer.write(b"x").is_err()); - assert!(writer.flush().is_err()); -} - -#[test] -fn rotation_helpers_handle_empty_and_missing_files() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("missing.log"); - rotate_files(&path, 2).unwrap(); - assert_eq!( - rotated_log_path(&path, 2), - directory.path().join("missing.2.log") - ); - create_parent_directory(std::path::Path::new("plain.log")).unwrap(); -} diff --git a/crates/core/tests/coverage/logging_sink_tests.rs b/crates/core/tests/coverage/logging_sink_tests.rs index 88f142655..044ce2b47 100644 --- a/crates/core/tests/coverage/logging_sink_tests.rs +++ b/crates/core/tests/coverage/logging_sink_tests.rs @@ -2,16 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use super::{ - DROP_REPORT_INTERVAL_MILLIS, DropNoticeRateLimiter, build_logger, dropped_record_error_handler, - log_level_filter, now_millis, reserved_sink_paths, resolve_log_path, rotated_log_path, - spdlog_level, stderr_error_handler, + DROP_REPORT_INTERVAL_MILLIS, DropNoticeRateLimiter, dropped_record_error_handler, + log_level_filter, now_millis, spdlog_level, stderr_error_handler, }; use crate::logging::LogLevel; -use crate::logging::{ - FileLogRotationConfig, FileLogSinkConfig, LogFormat, LogSinkConfig, LoggingConfig, - MAX_FILE_SINK_QUEUE_ENTRIES, -}; -use std::path::PathBuf; #[test] fn drop_notice_rate_limiter_reports_immediately_then_once_per_interval() { @@ -39,60 +33,3 @@ fn sink_helpers_cover_boundary_levels_time_and_emergency_handlers() { "expected test error", ))); } - -#[test] -fn logger_builder_rejects_duplicate_conflicting_and_invalid_file_sinks() { - let directory = tempfile::tempdir().unwrap(); - let log_path = directory.path().join("relay.log"); - let file_sink = |path: PathBuf, rotation| { - LogSinkConfig::File(FileLogSinkConfig { - path, - level: LogLevel::Info, - format: LogFormat::Jsonl, - queue_capacity: 8, - rotation, - }) - }; - - assert!(resolve_log_path(std::path::Path::new("")).is_err()); - let rotation = FileLogRotationConfig::new(32, 1).unwrap(); - assert_eq!(reserved_sink_paths(&log_path, Some(rotation)).len(), 2); - - let duplicate = LoggingConfig { - sinks: vec![ - file_sink(log_path.clone(), None), - file_sink(log_path.clone(), None), - ], - ..LoggingConfig::default() - }; - let error = match build_logger(&duplicate, "root".into()) { - Ok(_) => panic!("duplicate file sinks must be rejected"), - Err(error) => error, - }; - assert!(error.to_string().contains("duplicate logging sink path")); - - let conflict = LoggingConfig { - sinks: vec![ - file_sink(log_path.clone(), Some(rotation)), - file_sink(rotated_log_path(&log_path, 1), None), - ], - ..LoggingConfig::default() - }; - let error = match build_logger(&conflict, "root".into()) { - Ok(_) => panic!("active and rotated file paths must not overlap"), - Err(error) => error, - }; - assert!(error.to_string().contains("conflicts")); - - let mut invalid_capacity = LoggingConfig { - sinks: vec![file_sink(log_path, None)], - ..LoggingConfig::default() - }; - let LogSinkConfig::File(file_sink) = &mut invalid_capacity.sinks[0]; - file_sink.queue_capacity = MAX_FILE_SINK_QUEUE_ENTRIES + 1; - let error = match build_logger(&invalid_capacity, "root".into()) { - Ok(_) => panic!("oversized async queues must be rejected"), - Err(error) => error, - }; - assert!(error.to_string().contains("queue_capacity")); -} diff --git a/crates/core/tests/integration/pipeline_tests.rs b/crates/core/tests/integration/pipeline_tests.rs index 9711974de..ca8303bef 100644 --- a/crates/core/tests/integration/pipeline_tests.rs +++ b/crates/core/tests/integration/pipeline_tests.rs @@ -1972,3 +1972,45 @@ async fn test_stream_response_codec_annotation_uses_sanitized_aggregated_respons deregister_subscriber("stream_sanitized_resp_codec_sub").unwrap(); deregister_llm_sanitize_response_guardrail("stream_sanitize_resp_codec_annotation").unwrap(); } + +#[tokio::test] +async fn test_stream_response_sanitizer_can_flush_subscribers() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + register_subscriber("stream_reentrant_flush_subscriber", Arc::new(|_| {})).unwrap(); + register_llm_sanitize_response_guardrail( + "stream_reentrant_flush_sanitizer", + 1, + Arc::new(|response, _context| { + Box::pin(async move { + flush_subscribers()?; + Ok(Some(response)) + }) + }), + ) + .unwrap(); + + let mut stream = llm_stream_call_execute( + LlmStreamCallExecuteParams::builder() + .name("stream_reentrant_flush") + .request(make_openai_chat_request("stream me")) + .func(noop_stream_exec_fn()) + .collector(Box::new(|_chunk| Ok(()))) + .finalizer(Box::new(|| make_openai_chat_response("done"))) + .build(), + ) + .await + .unwrap(); + + while stream.next().await.is_some() {} + tokio::time::timeout(std::time::Duration::from_secs(2), stream.close()) + .await + .expect("stream close deadlocked in response sanitizer") + .unwrap(); + flush_subscribers().unwrap(); + + deregister_llm_sanitize_response_guardrail("stream_reentrant_flush_sanitizer").unwrap(); + deregister_subscriber("stream_reentrant_flush_subscriber").unwrap(); +} diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 9c00ee254..40e9a9075 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -2918,8 +2918,8 @@ pub fn deregister_tool_execution_intercept(name: String) -> Result { /// /// The `guardrail` callback receives `(request, context)` and must return the sanitized request, /// or `null` to omit the observability payload. Lower `priority` values run first. Throws if a -/// guardrail with the same `name` already exists. If the callback throws, Relay omits the payload -/// and records the error for `getLastCallbackError()`. +/// guardrail with the same `name` already exists. If the callback throws, Relay preserves the last +/// valid payload, continues publication, and records the error for `getLastCallbackError()`. #[napi] pub fn register_llm_sanitize_request_guardrail( env: Env, @@ -2952,8 +2952,8 @@ pub fn deregister_llm_sanitize_request_guardrail(name: String) -> Result { /// /// The `guardrail` callback receives `(response, context)` and must return the sanitized response, /// or `null` to omit the observability payload. Lower `priority` values run first. Throws if a -/// guardrail with the same `name` already exists. If the callback throws, Relay omits the payload -/// and records the error for `getLastCallbackError()`. +/// guardrail with the same `name` already exists. If the callback throws, Relay preserves the last +/// valid payload, continues publication, and records the error for `getLastCallbackError()`. #[napi] pub fn register_llm_sanitize_response_guardrail( env: Env, @@ -3174,8 +3174,9 @@ pub fn deregister_subscriber(name: String) -> Result { /// Return a Promise that resolves when native subscriber callbacks queued /// before this call finish. /// -/// When called from an event-sanitizer callback, this Promise resolves without waiting to prevent -/// a cycle with the serial dispatcher. +/// When called from a queued publication sanitizer callback (including event and manual tool/LLM +/// sanitizers), this Promise resolves without waiting to prevent a cycle with the serial +/// dispatcher. /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this /// Promise does not block the Node event loop while Promise-returning event sanitizers settle. @@ -3502,7 +3503,8 @@ pub fn scope_deregister_tool_execution_intercept(scope_uuid: String, name: Strin /// The `guardrail` callback receives `(request, context)` and must return the sanitized request, /// or `null` to omit the observability payload. Lower `priority` values run first. Throws if a /// guardrail with the same `name` already exists on the specified scope. If the callback throws, -/// Relay omits the payload and records the error for `getLastCallbackError()`. +/// Relay preserves the last valid payload, continues publication, and records the error for +/// `getLastCallbackError()`. #[napi] pub fn scope_register_llm_sanitize_request_guardrail( env: Env, @@ -3546,7 +3548,8 @@ pub fn scope_deregister_llm_sanitize_request_guardrail( /// The `guardrail` callback receives `(response, context)` and must return the sanitized response, /// or `null` to omit the observability payload. Lower `priority` values run first. Throws if a /// guardrail with the same `name` already exists on the specified scope. If the callback throws, -/// Relay omits the payload and records the error for `getLastCallbackError()`. +/// Relay preserves the last valid payload, continues publication, and records the error for +/// `getLastCallbackError()`. #[napi] pub fn scope_register_llm_sanitize_response_guardrail( env: Env, diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index 905f77e49..9d08f1ba0 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -284,12 +284,17 @@ pub fn wrap_js_tool_request_intercept_promise_fn(func: Arc) -> T pub fn wrap_js_tool_sanitize_promise_fn(func: Arc) -> ToolSanitizeFn { Arc::new(move |name: String, value: Json| { let func = func.clone(); + let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { - func.call_spread(vec![Json::String(name), value]) - .await - .inspect_err(|error| { - record_callback_error(error.to_string()); - }) + let args = vec![Json::String(name), value]; + let result = if publication { + func.call_spread_for_publication(args).await + } else { + func.call_spread(args).await + }; + result.inspect_err(|error| { + record_callback_error(error.to_string()); + }) }) }) } diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index df9a7785a..40f780bc0 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -248,6 +248,12 @@ impl PromiseAwareFn { .await } + /// Call a spread callback from queued event publication. + pub async fn call_spread_for_publication(&self, args: Vec) -> FlowResult { + self.call_inner(PrimaryArg::Json(Json::Array(args)), true, None, true) + .await + } + /// Call the JS function with a builder-constructed first argument and await /// the result. /// diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index b54939a2d..c2bad301d 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -568,6 +568,40 @@ describe('LLM guardrails', () => { } }); + it('stream response sanitizers can flush subscribers without deadlocking', async () => { + let responseFlushed = false; + registerSubscriber('node_stream_flush_subscriber', () => {}); + registerLlmSanitizeResponseGuardrail('node_stream_flush_response', 10, async (response) => { + await flushSubscribers(); + responseFlushed = true; + return response; + }); + try { + const stream = await llmStreamCallExecute( + 'node_stream_flush', + makeNative(), + (wrapper) => { + lib.pushStreamChunk(wrapper.__nemo_relay_stream_id, { delta: 'ok' }); + lib.endStream(wrapper.__nemo_relay_stream_id); + }, + null, + () => ({ response: 'ok' }), + null, + null, + null, + null, + null, + ); + assert.deepEqual(await stream.next(), { delta: 'ok' }); + assert.equal(await stream.next(), null); + await flushSubscribers(); + } finally { + deregisterLlmSanitizeResponseGuardrail('node_stream_flush_response'); + deregisterSubscriber('node_stream_flush_subscriber'); + } + assert.equal(responseFlushed, true); + }); + it('releases custom stream codec references safely after early garbage collection', () => { const modulePath = path.join(nodeDir, 'index.js'); const script = ` diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index c9884bc05..4576d8253 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -709,6 +709,40 @@ describe('Tool guardrails', () => { } }); + it('manual async sanitizers can flush subscribers without deadlocking', async () => { + const events = []; + let requestFlushed = false; + let responseFlushed = false; + registerSubscriber('node_manual_tool_flush_subscriber', (event) => events.push(event)); + registerToolSanitizeRequestGuardrail('node_manual_tool_flush_request', 10, async (_name, args) => { + await flushSubscribers(); + requestFlushed = true; + return { ...args, requestSanitized: true }; + }); + registerToolSanitizeResponseGuardrail('node_manual_tool_flush_response', 10, async (_name, response) => { + await flushSubscribers(); + responseFlushed = true; + return { ...response, responseSanitized: true }; + }); + try { + const handle = toolCall('node_manual_tool_flush', { original: true }); + toolCallEnd(handle, { ok: true }); + await flushSubscribers(); + } finally { + deregisterToolSanitizeRequestGuardrail('node_manual_tool_flush_request'); + deregisterToolSanitizeResponseGuardrail('node_manual_tool_flush_response'); + deregisterSubscriber('node_manual_tool_flush_subscriber'); + } + assert.equal(requestFlushed, true); + assert.equal(responseFlushed, true); + const start = events.find( + (event) => event.name === 'node_manual_tool_flush' && event.scope_category === 'start', + ); + const end = events.find((event) => event.name === 'node_manual_tool_flush' && event.scope_category === 'end'); + assert.deepEqual(start.data, { original: true, requestSanitized: true }); + assert.deepEqual(end.data, { ok: true, responseSanitized: true }); + }); + it('conditional guardrail (block)', () => { registerToolConditionalExecutionGuardrail('node_tool_block', 10, (name, args) => 'blocked'); deregisterToolConditionalExecutionGuardrail('node_tool_block'); diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index ce23c5ec3..279040388 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -469,18 +469,30 @@ fn stream_from_async_iter(async_iter: Py) -> FlowResult { /// Wrap a Python callable `(str, Json) -> Json` for tool sanitize/intercept fns. pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new(move |name: String, args: Json| { let py_fn = py_fn.clone(); + let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { - resolve_json_or_future(Python::attach(|py| { + let result = resolve_py_object_or_future(Python::attach(|py| { let py_args = json_to_py(py, &args) .map_err(|e| FlowError::Internal(format!("tool json_to_py failed: {e}")))?; - let result = py_fn.call1(py, (name, py_args)).map_err(|e| { - FlowError::Internal(format!("Python tool callback failed: {e}")) - })?; - split_json_or_future(py, result) + let result = if publication { + py.import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("invoke")) + .and_then(|invoke| invoke.call1((py_fn.bind(py), name, py_args))) + } else { + py_fn.bind(py).call1((name, py_args)) + } + .map_err(|e| FlowError::Internal(format!("Python tool callback failed: {e}")))?; + split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) })) - .await + .await?; + Python::attach(|py| { + py_to_json(result.bind(py)) + .map_err(|e| FlowError::Internal(format!("tool py_to_json failed: {e}"))) + }) }) }) } diff --git a/docs/about-nemo-relay/concepts/middleware.mdx b/docs/about-nemo-relay/concepts/middleware.mdx index 1af705901..f74a3bb89 100644 --- a/docs/about-nemo-relay/concepts/middleware.mdx +++ b/docs/about-nemo-relay/concepts/middleware.mdx @@ -328,14 +328,18 @@ register_llm_sanitize_request_guardrail( "redact-openai-chat", 10, Arc::new(|mut request, context| { - if context.codec() == &LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat) - && let Some(codec) = context.resolve_codec() - && let Ok(mut annotated) = codec.decode(&request) - { - annotated.messages.clear(); - request = codec.encode(&annotated, &request).ok()?; - } - Some(request) + Box::pin(async move { + if context.codec() == &LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat) + && let Some(codec) = context.resolve_codec() + && let Ok(mut annotated) = codec.decode(&request) + { + annotated.messages.clear(); + if let Ok(encoded) = codec.encode(&annotated, &request) { + request = encoded; + } + } + Ok(Some(request)) + }) }), )?; ``` diff --git a/docs/instrument-applications/advanced-guide.mdx b/docs/instrument-applications/advanced-guide.mdx index 87c41c380..38e05d264 100644 --- a/docs/instrument-applications/advanced-guide.mdx +++ b/docs/instrument-applications/advanced-guide.mdx @@ -124,12 +124,14 @@ register_tool_sanitize_request_guardrail( "search.redact_api_key", 10, Arc::new(|_tool_name, mut args| { - if let Some(object) = args.as_object_mut() { - if object.contains_key("api_key") { - object.insert("api_key".into(), json!("")); + Box::pin(async move { + if let Some(object) = args.as_object_mut() { + if object.contains_key("api_key") { + object.insert("api_key".into(), json!("")); + } } - } - args + Ok(args) + }) }), )?; @@ -137,9 +139,11 @@ register_tool_conditional_execution_guardrail( "search.require_query", 20, Arc::new(|_tool_name, args| { - Ok(match args.get("query").and_then(|value| value.as_str()) { - Some(query) if !query.is_empty() => None, - _ => Some("search.query is required".into()), + Box::pin(async move { + Ok(match args.get("query").and_then(|value| value.as_str()) { + Some(query) if !query.is_empty() => None, + _ => Some("search.query is required".into()), + }) }) }), )?; diff --git a/docs/reference/event-sanitizers.mdx b/docs/reference/event-sanitizers.mdx index 6122394b6..9ebeb5eae 100644 --- a/docs/reference/event-sanitizers.mdx +++ b/docs/reference/event-sanitizers.mdx @@ -135,9 +135,11 @@ register_mark_sanitize_guardrail( "safe-marks", 100, Arc::new(|event, mut fields| { - fields.data = Some(json!({"checkpoint": event.name()})); - fields.metadata = None; - fields + Box::pin(async move { + fields.data = Some(json!({"checkpoint": event.name()})); + fields.metadata = None; + Ok(fields) + }) }), )?; diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index e344cf738..4fc5529e0 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -72,11 +72,15 @@ Python and Node.js registration names are unchanged. Mark a Python callback `async def`, or return a Promise from Node.js, only when it needs asynchronous work; existing direct-value callbacks remain supported. -Scope lifecycle and mark emission remain synchronous. `push_scope`, -`pop_scope`, and mark APIs snapshot the event and visible sanitizer/subscriber -chain, then enqueue sanitization and publication on a serial dispatcher. Event -subscribers and exporters therefore receive sanitized events later, in emission -order. Do not add `await` to scope or mark emission calls. +Scope, mark, and manual tool/LLM lifecycle APIs remain synchronous. This +includes `push_scope`, `pop_scope`, mark APIs, `tool_call`, `tool_call_end`, +`llm_call`, and `llm_call_end`. These APIs snapshot the event and visible +sanitizer/subscriber chain, then enqueue sanitization and publication on a +serial dispatcher. Event subscribers and exporters therefore receive sanitized +events later, in emission order. Do not add `await` to these lifecycle calls. +Only enqueue-time validation and runtime-state errors are returned directly; +middleware and codec errors discovered during queued publication are logged and +handled according to their fail-open contracts. ### Update LLM Sanitizer Callbacks @@ -171,8 +175,9 @@ Check every callback for an implicit empty return. In particular: - A Python function that reaches the end without `return` omits the payload. - A JavaScript callback that returns `null` or `undefined` omits the payload. - A Rust callback must return `Some(payload)` to retain the payload. -- A sanitizer error reported through a plugin or binding boundary also omits - the payload and annotation. +- A sanitizer error or panic fails open: Relay preserves the last valid payload + and annotation snapshot, logs or records the callback error, and continues + publication. Use omission only when recording the payload would be unsafe. diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index d5a60cc7c..ebd6b2975 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -95,9 +95,9 @@ def flush() -> None: waiting for observer work. Use this barrier in tests and shutdown paths when captured subscriber output must be complete before continuing. - Call this function outside subscriber and event-sanitizer callbacks. A - re-entrant call returns without waiting to avoid blocking the dispatcher, - so callbacks later in the same dispatch snapshot can still run. + Call this function outside subscriber and queued publication sanitizer + callbacks. A re-entrant call returns without waiting to avoid blocking the + dispatcher, so callbacks later in the same dispatch snapshot can still run. """ if _event_sanitizer_callback_active(): return None diff --git a/python/tests/test_tools.py b/python/tests/test_tools.py index a401eb73b..f6d74c084 100644 --- a/python/tests/test_tools.py +++ b/python/tests/test_tools.py @@ -3,6 +3,7 @@ """Tests for NeMo Relay tool lifecycle, guardrails, and intercepts.""" +import asyncio from collections import UserDict, UserList from dataclasses import dataclass from typing import cast @@ -315,6 +316,48 @@ def test_deregister_nonexistent(self): class TestToolGuardrailsAsync: + async def test_manual_async_sanitizers_publish_transformed_payloads_and_can_flush(self): + events = [] + request_flushed = False + response_flushed = False + + async def sanitize_request(name, args): + nonlocal request_flushed + await asyncio.sleep(0) + subscribers.flush() + request_flushed = True + return {**args, "request_sanitized": True} + + async def sanitize_response(name, response): + nonlocal response_flushed + await asyncio.sleep(0) + subscribers.flush() + response_flushed = True + return {**response, "response_sanitized": True} + + subscribers.register("py_manual_tool_flush_subscriber", events.append) + guardrails.register_tool_sanitize_request("py_manual_tool_flush_request", 1, sanitize_request) + guardrails.register_tool_sanitize_response("py_manual_tool_flush_response", 1, sanitize_response) + try: + handle = tools.call("py_manual_tool_flush", {"original": True}) + tools.call_end(handle, {"ok": True}) + await asyncio.wait_for(asyncio.to_thread(subscribers.flush), timeout=2) + finally: + guardrails.deregister_tool_sanitize_request("py_manual_tool_flush_request") + guardrails.deregister_tool_sanitize_response("py_manual_tool_flush_response") + subscribers.deregister("py_manual_tool_flush_subscriber") + + assert request_flushed + assert response_flushed + assert _tool_event(events, "py_manual_tool_flush", "start").data == { + "original": True, + "request_sanitized": True, + } + assert _tool_event(events, "py_manual_tool_flush", "end").data == { + "ok": True, + "response_sanitized": True, + } + async def test_conditional_blocks_execution(self): guardrails.register_tool_conditional_execution("py_async_blocker", 1, lambda name, args: "blocked by policy") From bc7638891ed119f7ac8664721f96cbf3d8e970db Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 23:11:17 -0400 Subject: [PATCH 37/83] fix: make publication barriers flush-safe Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 13 +++-- .../src/api/runtime/subscriber_dispatcher.rs | 49 +++++++++++++++---- crates/core/src/api/subscriber.rs | 7 +-- crates/core/src/api/tool.rs | 20 +++++--- .../core/tests/integration/pipeline_tests.rs | 16 +++--- crates/node/src/api/mod.rs | 7 +-- crates/python/src/py_api/mod.rs | 5 +- python/nemo_relay/subscribers.py | 6 +-- 8 files changed, 83 insertions(+), 40 deletions(-) diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index 1472cf66d..dc2e68846 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -691,11 +691,13 @@ fn emit_optimization_marks_with( /// the emitted start event. When `None`, the current UTC time is used. /// /// # Returns -/// A [`Result`] containing the created [`LlmHandle`]. +/// A [`Result`] containing the created [`LlmHandle`] after its start-event +/// snapshot has been submitted for queued publication. /// /// # Errors /// Returns an error when the runtime owner check fails or when internal state -/// cannot be read safely. +/// cannot be read safely. Dispatcher submission failures are logged because +/// observability publication is best effort. /// /// # Notes /// The runtime removes standard credential headers (`authorization`, @@ -892,9 +894,10 @@ async fn build_llm_end_payload( /// sanitization and publication. /// /// # Errors -/// Returns an error when the runtime owner check fails, internal state cannot be -/// read safely, or the event cannot be queued. Sanitizer and response-codec -/// errors discovered during queued publication are logged and fail open. +/// Returns an error when the runtime owner check fails or internal state cannot +/// be read safely. Dispatcher submission failures are logged because +/// observability publication is best effort. Sanitizer and response-codec errors +/// discovered during queued publication are also logged and fail open. /// /// # Notes /// Sanitize-response guardrails affect only the emitted end-event payload, not diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 651d57848..8e16e927a 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -23,6 +23,7 @@ mod native { use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; + use std::time::Duration; use super::*; use crate::api::runtime::scope_stack::{ @@ -278,13 +279,37 @@ mod native { } } DispatcherMessage::Barrier { done } => { - let _ = done.recv(); + wait_for_barrier(done, &rx, &mut pending); } message => handle_message(message), } } } + /// Preserve FIFO delivery behind an asynchronous publication boundary while + /// allowing flush requests to return. A flush cannot wait for the current + /// publication without risking a cycle when middleware spawned the caller. + fn wait_for_barrier( + done: Receiver<()>, + rx: &Receiver, + pending: &mut VecDeque, + ) { + loop { + match done.recv_timeout(Duration::from_millis(10)) { + Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => return, + Err(mpsc::RecvTimeoutError::Timeout) => {} + } + while let Ok(message) = rx.try_recv() { + match message { + DispatcherMessage::Flush { done } => { + let _ = done.send(()); + } + message => pending.push_back(message), + } + } + } + } + fn drain_pending_messages( rx: &Receiver, pending: &mut VecDeque, @@ -412,14 +437,13 @@ mod native { #[cfg(test)] mod tests { use super::*; - use std::sync::Mutex; - use std::time::Duration; - - static TEST_MUTEX: Mutex<()> = Mutex::new(()); #[test] - fn flush_does_not_wait_for_a_later_publication_barrier() { - let _lock = TEST_MUTEX.lock().unwrap(); + fn flush_does_not_wait_for_active_or_later_publication_barriers() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + flush_subscribers().unwrap(); let first = register_async_publication().expect("first publication barrier"); let sender = dispatcher_sender().expect("dispatcher sender"); let (flush_tx, flush_rx) = mpsc::channel(); @@ -428,11 +452,12 @@ mod native { .unwrap(); let later = register_async_publication().expect("later publication barrier"); - first.send(()).unwrap(); flush_rx .recv_timeout(Duration::from_secs(1)) - .expect("flush queued before the later barrier must complete"); + .expect("flush must not wait for an active publication barrier"); + first.send(()).unwrap(); later.send(()).unwrap(); + flush_subscribers().unwrap(); } } } @@ -488,7 +513,11 @@ pub(crate) async fn with_async_publication_context(future: F) -> F::O native::with_async_publication_context(future).await } -/// Wait for all queued subscriber callbacks submitted before this call. +/// Wait for queued subscriber callbacks submitted before this call. +/// +/// If an asynchronous publication boundary is still active, this returns +/// without waiting for that publication or work queued behind it. This avoids +/// a cycle when publication middleware spawns or offloads the caller. pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() } diff --git a/crates/core/src/api/subscriber.rs b/crates/core/src/api/subscriber.rs index 02ce9a322..1c8cf2b95 100644 --- a/crates/core/src/api/subscriber.rs +++ b/crates/core/src/api/subscriber.rs @@ -72,9 +72,10 @@ pub fn deregister_subscriber(name: &str) -> Result { /// Wait for all subscriber callbacks queued before this call to finish. /// -/// Call this helper outside native subscriber callbacks. A re-entrant call returns without -/// waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can -/// still run. +/// A re-entrant call returns without waiting. The same applies while an +/// asynchronous publication boundary is active, including calls spawned or +/// offloaded by publication middleware. Call again after that middleware +/// settles to wait for its event and work queued behind it. /// /// Native targets deliver subscriber callbacks on a background dispatcher so /// event-producing APIs do not wait for observer work. Call this helper from diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 30a9249e1..14b974651 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -202,8 +202,8 @@ pub struct ToolCallEndParams<'a> { /// Start a manual tool lifecycle span. /// -/// This emits a tool-start event after applying sanitize-request guardrails to -/// the payload recorded for observability. +/// This submits a tool-start event for queued sanitize-request guardrails and +/// publication without waiting for that work. /// /// # Parameters /// - `name`: Tool name recorded on the emitted lifecycle event. @@ -218,11 +218,13 @@ pub struct ToolCallEndParams<'a> { /// the emitted start event. When `None`, the current UTC time is used. /// /// # Returns -/// A [`Result`] containing the created [`ToolHandle`]. +/// A [`Result`] containing the created [`ToolHandle`] after its start-event +/// snapshot has been submitted for queued publication. /// /// # Errors /// Returns an error when the runtime owner check fails or when internal state -/// cannot be read safely. +/// cannot be read safely. Dispatcher submission failures are logged because +/// observability publication is best effort. /// /// # Notes /// Sanitize-request guardrails affect only the emitted start-event payload, not @@ -392,8 +394,8 @@ async fn tool_call_with_subscriber_snapshot( /// Finish a manual tool lifecycle span. /// -/// This emits a tool-end event for a handle previously returned by -/// [`tool_call`]. +/// This submits a tool-end event for queued sanitization and publication for a +/// handle previously returned by [`tool_call`]. /// /// # Parameters /// - `handle`: Tool handle to close. @@ -407,11 +409,13 @@ async fn tool_call_with_subscriber_snapshot( /// the handle start time if the current time is not later. /// /// # Returns -/// A [`Result`] that is `Ok(())` when the end event has been emitted. +/// A [`Result`] that is `Ok(())` when the end-event snapshot has been submitted +/// for queued publication. /// /// # Errors /// Returns an error when the runtime owner check fails or when internal state -/// cannot be read safely. +/// cannot be read safely. Dispatcher submission failures are logged because +/// observability publication is best effort. /// /// # Notes /// Sanitize-response guardrails affect only the emitted end-event payload, not diff --git a/crates/core/tests/integration/pipeline_tests.rs b/crates/core/tests/integration/pipeline_tests.rs index ca8303bef..bff17d6a7 100644 --- a/crates/core/tests/integration/pipeline_tests.rs +++ b/crates/core/tests/integration/pipeline_tests.rs @@ -1985,7 +1985,9 @@ async fn test_stream_response_sanitizer_can_flush_subscribers() { 1, Arc::new(|response, _context| { Box::pin(async move { - flush_subscribers()?; + tokio::task::spawn_blocking(flush_subscribers) + .await + .map_err(|error| FlowError::Internal(error.to_string()))??; Ok(Some(response)) }) }), @@ -2004,11 +2006,13 @@ async fn test_stream_response_sanitizer_can_flush_subscribers() { .await .unwrap(); - while stream.next().await.is_some() {} - tokio::time::timeout(std::time::Duration::from_secs(2), stream.close()) - .await - .expect("stream close deadlocked in response sanitizer") - .unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(2), async { + while stream.next().await.is_some() {} + stream.close().await + }) + .await + .expect("stream finalization deadlocked in response sanitizer") + .unwrap(); flush_subscribers().unwrap(); deregister_llm_sanitize_response_guardrail("stream_reentrant_flush_sanitizer").unwrap(); diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 40e9a9075..c1e6ecdd3 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -3174,9 +3174,10 @@ pub fn deregister_subscriber(name: String) -> Result { /// Return a Promise that resolves when native subscriber callbacks queued /// before this call finish. /// -/// When called from a queued publication sanitizer callback (including event and manual tool/LLM -/// sanitizers), this Promise resolves without waiting to prevent a cycle with the serial -/// dispatcher. +/// When called from queued publication middleware, or while an asynchronous +/// publication boundary is active, this Promise resolves without waiting. +/// Call it again after the middleware settles to wait for its event and later +/// work. /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this /// Promise does not block the Node event loop while Promise-returning event sanitizers settle. diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 7bb90a042..0e00bab51 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1524,8 +1524,9 @@ fn deregister_subscriber(name: &str) -> PyResult { /// Wait for subscriber callbacks queued before this call to finish. /// -/// Public Python wrappers prevent re-entrant event-sanitizer callbacks from waiting on the serial -/// dispatcher. +/// Re-entrant calls and calls observed while an asynchronous publication +/// boundary is active return without waiting. Call again after middleware +/// settles to wait for its event and later work. #[pyfunction] fn flush_subscribers(py: Python<'_>) -> PyResult<()> { py.detach(core_subscriber_api::flush_subscribers) diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index ebd6b2975..1137fc829 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -95,9 +95,9 @@ def flush() -> None: waiting for observer work. Use this barrier in tests and shutdown paths when captured subscriber output must be complete before continuing. - Call this function outside subscriber and queued publication sanitizer - callbacks. A re-entrant call returns without waiting to avoid blocking the - dispatcher, so callbacks later in the same dispatch snapshot can still run. + A re-entrant call, or a call observed while an asynchronous publication + boundary is active, returns without waiting. Call ``flush()`` again after + that middleware settles to wait for its event and later work. """ if _event_sanitizer_callback_active(): return None From 2204cbd272a108d0f86f4bd5d2b7460775c3510e Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 23:29:52 -0400 Subject: [PATCH 38/83] fix: preserve async publication flush ordering Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 22 +- .../src/api/runtime/subscriber_dispatcher.rs | 251 ++++++++++++------ crates/core/src/api/subscriber.rs | 8 +- crates/core/src/stream.rs | 12 +- .../core/tests/integration/pipeline_tests.rs | 123 ++++++++- crates/node/src/api/mod.rs | 8 +- crates/python/src/py_api/mod.rs | 6 +- python/nemo_relay/subscribers.py | 7 +- 8 files changed, 327 insertions(+), 110 deletions(-) diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index dc2e68846..cfd107173 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -21,7 +21,7 @@ use crate::api::runtime::LlmCodecIdentity; use crate::api::runtime::NemoRelayContextState; use crate::api::runtime::global_context; use crate::api::runtime::subscriber_dispatcher::{ - dispatch_sanitized_event, dispatch_transformed_event, + dispatch_reserved_sanitized_event, dispatch_sanitized_event, dispatch_transformed_event, }; use crate::api::runtime::{ EventSubscriberFn, LlmCollectorFn, LlmExecutionNextFn, LlmFinalizerFn, LlmJsonStream, @@ -547,6 +547,26 @@ pub(crate) async fn emit_optimization_marks(handle: &LlmHandle, subscribers: &[E .await; } +pub(crate) async fn emit_reserved_optimization_marks( + handle: &LlmHandle, + subscribers: &[EventSubscriberFn], +) { + emit_optimization_marks_with_async( + handle, + subscribers, + |event| sanitize_event_with_scope_stack(event, handle.captured_scope_stack()), + |event, subscribers| { + dispatch_reserved_sanitized_event( + event.clone(), + Vec::new(), + subscribers, + handle.captured_scope_stack().clone(), + ) + }, + ) + .await; +} + /// Queue optimization marks from a synchronous lifecycle API. /// /// The public manual lifecycle APIs must not await middleware. Capture each diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 8e16e927a..fc77a9bcc 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -17,13 +17,12 @@ pub(crate) type EventTransformFn = Box< >; mod native { - use std::cell::Cell; + use std::cell::{Cell, RefCell}; use std::collections::VecDeque; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; - use std::time::Duration; use super::*; use crate::api::runtime::scope_stack::{ @@ -44,7 +43,7 @@ mod native { done: Sender<()>, }, Barrier { - done: Receiver<()>, + publications: Receiver>, }, } @@ -58,11 +57,15 @@ mod native { static IN_DISPATCHER: Cell = const { Cell::new(false) }; } tokio::task_local! { - static IN_ASYNC_PUBLICATION: (); + static ASYNC_PUBLICATION_MESSAGES: RefCell>>; } struct DispatchGuard; + pub(crate) struct AsyncPublication { + sender: Sender>, + } + impl DispatchGuard { fn enter() -> Self { IN_DISPATCHER.with(|flag| flag.set(true)); @@ -106,31 +109,7 @@ mod native { subscribers: subscribers.to_vec(), scope_stack: current_scope_stack(), }; - match dispatcher_sender() { - Ok(sender) => { - if sender.send(message).is_err() { - log::warn!( - target: "nemo_relay.runtime", - event = "subscriber_event_dropped", - reason = "dispatcher_disconnected"; - "Subscriber event was dropped because the dispatcher stopped" - ); - false - } else { - true - } - } - Err(_error) if !DISPATCHER_FAILURE_LOGGED.swap(true, Ordering::AcqRel) => { - log::error!( - target: "nemo_relay.runtime", - event = "subscriber_dispatcher_failed", - error_kind = "initialization"; - "Subscriber dispatcher failed to start" - ); - false - } - Err(_) => false, - } + send_dispatch_message(message) } pub(super) fn dispatch_sanitized_event( @@ -152,6 +131,39 @@ mod native { send_dispatch_message(message) } + pub(super) fn dispatch_reserved_sanitized_event( + event: Event, + sanitizers: Vec>, + subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, + ) -> bool { + if subscribers.is_empty() { + return true; + } + let message = DispatcherMessage::Deliver { + event: Box::new(event), + transform: None, + sanitizers, + subscribers: subscribers.to_vec(), + scope_stack, + }; + let buffer_active = ASYNC_PUBLICATION_MESSAGES + .try_with(|messages| messages.borrow().is_some()) + .unwrap_or(false); + if buffer_active { + ASYNC_PUBLICATION_MESSAGES.with(|messages| { + messages + .borrow_mut() + .as_mut() + .expect("publication buffer checked above") + .push(message); + }); + true + } else { + send_dispatch_message(message) + } + } + pub(super) fn dispatch_transformed_event( event: Event, transform: EventTransformFn, @@ -169,16 +181,20 @@ mod native { send_dispatch_message(message) } - /// Insert a FIFO barrier for work that will enqueue a publication from an - /// async task. A later flush waits for the task to signal completion, then - /// drains the event it queued before acknowledging the flush. - pub(super) fn register_async_publication() -> Option> { + /// Reserve a FIFO position for publications produced by an async task. + /// A later flush waits for the task and drains its buffered publications + /// at the reserved position before acknowledging the flush. + pub(super) fn register_async_publication() -> Option { let sender = dispatcher_sender().ok()?; - let (done_tx, done_rx) = mpsc::channel(); + let (publication_tx, publication_rx) = mpsc::channel(); sender - .send(DispatcherMessage::Barrier { done: done_rx }) + .send(DispatcherMessage::Barrier { + publications: publication_rx, + }) .ok() - .map(|_| done_tx) + .map(|_| AsyncPublication { + sender: publication_tx, + }) } pub(super) fn flush_subscribers() -> Result<()> { @@ -204,14 +220,31 @@ mod native { } pub(super) fn in_dispatcher_callback() -> bool { - IN_DISPATCHER.with(Cell::get) || IN_ASYNC_PUBLICATION.try_with(|_| ()).is_ok() + IN_DISPATCHER.with(Cell::get) || ASYNC_PUBLICATION_MESSAGES.try_with(|_| ()).is_ok() } - pub(super) async fn with_async_publication_context(future: F) -> F::Output { - if IN_ASYNC_PUBLICATION.try_with(|_| ()).is_ok() { + pub(super) async fn with_async_publication_context( + publication: Option, + future: F, + ) -> F::Output { + if ASYNC_PUBLICATION_MESSAGES.try_with(|_| ()).is_ok() { future.await } else { - IN_ASYNC_PUBLICATION.scope((), future).await + let (output, publications) = ASYNC_PUBLICATION_MESSAGES + .scope( + RefCell::new(publication.as_ref().map(|_| Vec::new())), + async { + let output = future.await; + let publications = ASYNC_PUBLICATION_MESSAGES + .with(|messages| messages.borrow_mut().take()); + (output, publications) + }, + ) + .await; + if let (Some(publication), Some(publications)) = (publication, publications) { + let _ = publication.sender.send(publications); + } + output } } @@ -278,34 +311,14 @@ mod native { let _ = pending.send(()); } } - DispatcherMessage::Barrier { done } => { - wait_for_barrier(done, &rx, &mut pending); - } - message => handle_message(message), - } - } - } - - /// Preserve FIFO delivery behind an asynchronous publication boundary while - /// allowing flush requests to return. A flush cannot wait for the current - /// publication without risking a cycle when middleware spawned the caller. - fn wait_for_barrier( - done: Receiver<()>, - rx: &Receiver, - pending: &mut VecDeque, - ) { - loop { - match done.recv_timeout(Duration::from_millis(10)) { - Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => return, - Err(mpsc::RecvTimeoutError::Timeout) => {} - } - while let Ok(message) = rx.try_recv() { - match message { - DispatcherMessage::Flush { done } => { - let _ = done.send(()); + DispatcherMessage::Barrier { publications } => { + if let Ok(publications) = publications.recv() { + for publication in publications { + handle_message(publication); + } } - message => pending.push_back(message), } + message => handle_message(message), } } } @@ -340,8 +353,12 @@ mod native { DispatcherMessage::Flush { done } => { let _ = done.send(()); } - DispatcherMessage::Barrier { done } => { - let _ = done.recv(); + DispatcherMessage::Barrier { publications } => { + if let Ok(publications) = publications.recv() { + for publication in publications { + handle_message(publication); + } + } } } } @@ -439,24 +456,79 @@ mod native { use super::*; #[test] - fn flush_does_not_wait_for_active_or_later_publication_barriers() { + fn flush_waits_for_active_but_not_later_publication_barriers() { let _lock = crate::shared_runtime::runtime_owner_test_mutex() .lock() .unwrap_or_else(|error| error.into_inner()); flush_subscribers().unwrap(); let first = register_async_publication().expect("first publication barrier"); let sender = dispatcher_sender().expect("dispatcher sender"); + let delivered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let subscriber: EventSubscriberFn = { + let delivered = delivered.clone(); + std::sync::Arc::new(move |event| { + delivered + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(event.name().to_string()); + }) + }; + let queued_event = serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000001", + "timestamp": "2026-07-28T00:00:00Z", + "name": "queued-before-flush" + })) + .expect("valid event"); + sender + .send(DispatcherMessage::Deliver { + event: Box::new(queued_event), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![subscriber.clone()], + scope_stack: current_scope_stack(), + }) + .unwrap(); let (flush_tx, flush_rx) = mpsc::channel(); sender .send(DispatcherMessage::Flush { done: flush_tx }) .unwrap(); let later = register_async_publication().expect("later publication barrier"); + assert!( + flush_rx + .recv_timeout(std::time::Duration::from_millis(50)) + .is_err(), + "flush must wait for an active publication barrier" + ); + let deferred_event = serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000002", + "timestamp": "2026-07-28T00:00:00Z", + "name": "deferred-at-barrier" + })) + .expect("valid event"); + first + .sender + .send(vec![DispatcherMessage::Deliver { + event: Box::new(deferred_event), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![subscriber], + scope_stack: current_scope_stack(), + }]) + .unwrap(); flush_rx - .recv_timeout(Duration::from_secs(1)) - .expect("flush must not wait for an active publication barrier"); - first.send(()).unwrap(); - later.send(()).unwrap(); + .recv_timeout(std::time::Duration::from_secs(1)) + .expect("flush queued before the later barrier must complete"); + assert_eq!( + *delivered.lock().unwrap_or_else(|error| error.into_inner()), + ["deferred-at-barrier", "queued-before-flush"], + "the barrier must publish deferred work at its reserved FIFO position" + ); + later.sender.send(Vec::new()).unwrap(); flush_subscribers().unwrap(); } } @@ -485,6 +557,16 @@ pub(crate) fn dispatch_sanitized_event( native::dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack) } +/// Publish a stream-finalization event at its reserved FIFO position. +pub(crate) fn dispatch_reserved_sanitized_event( + event: Event, + sanitizers: Vec>, + subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, +) -> bool { + native::dispatch_reserved_sanitized_event(event, sanitizers, subscribers, scope_stack) +} + /// Queue a snapshot for a middleware-specific asynchronous transformation, /// followed by event sanitization and subscriber delivery. pub(crate) fn dispatch_transformed_event( @@ -499,25 +581,26 @@ pub(crate) fn dispatch_transformed_event( /// Register a FIFO barrier for async work that will queue a subscriber event. /// -/// Dropping the returned sender releases the barrier, so error paths cannot -/// leave the dispatcher blocked. -pub(crate) fn register_async_publication() -> Option> { +/// Dropping the returned publication handle releases the barrier, so error +/// paths cannot leave the dispatcher blocked. +pub(crate) fn register_async_publication() -> Option { native::register_async_publication() } -/// Run asynchronous middleware as part of an already-registered publication. +/// Run asynchronous middleware as part of an already-registered publication, +/// buffering the finalization publications explicitly assigned to its reserved +/// FIFO position. /// /// Re-entrant subscriber flushes are no-ops in this context because the /// publication's FIFO barrier cannot complete until the middleware returns. -pub(crate) async fn with_async_publication_context(future: F) -> F::Output { - native::with_async_publication_context(future).await +pub(crate) async fn with_async_publication_context( + publication: Option, + future: F, +) -> F::Output { + native::with_async_publication_context(publication, future).await } -/// Wait for queued subscriber callbacks submitted before this call. -/// -/// If an asynchronous publication boundary is still active, this returns -/// without waiting for that publication or work queued behind it. This avoids -/// a cycle when publication middleware spawns or offloads the caller. +/// Wait for all queued subscriber callbacks submitted before this call. pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() } diff --git a/crates/core/src/api/subscriber.rs b/crates/core/src/api/subscriber.rs index 1c8cf2b95..337544167 100644 --- a/crates/core/src/api/subscriber.rs +++ b/crates/core/src/api/subscriber.rs @@ -72,10 +72,10 @@ pub fn deregister_subscriber(name: &str) -> Result { /// Wait for all subscriber callbacks queued before this call to finish. /// -/// A re-entrant call returns without waiting. The same applies while an -/// asynchronous publication boundary is active, including calls spawned or -/// offloaded by publication middleware. Call again after that middleware -/// settles to wait for its event and work queued behind it. +/// A direct re-entrant call from queued publication middleware returns without +/// waiting. Publication middleware must not move such a flush into +/// `tokio::spawn`, `tokio::task::spawn_blocking`, or another unmarked task or +/// thread because the publication cannot complete while awaiting that flush. /// /// Native targets deliver subscriber callbacks on a background dispatcher so /// event-producing APIs do not wait for observer work. Call this helper from diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index feaaace1e..a1c02179b 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -34,7 +34,7 @@ use tokio_stream::Stream; use crate::api::event::{BaseEvent, MarkEvent}; use crate::api::llm::LlmHandle; -use crate::api::llm::emit_optimization_marks; +use crate::api::llm::emit_reserved_optimization_marks; use crate::api::optimization::finalize_optimization_summary; use crate::api::runtime::LlmSanitizeResponseContext; use crate::api::runtime::NemoRelayContextState; @@ -295,7 +295,7 @@ impl LlmStreamWrapper { handle .optimization_recorder .close_for_finalization(interruption); - emit_optimization_marks(&handle, &subscribers).await; + emit_reserved_optimization_marks(&handle, &subscribers).await; let pricing = crate::codec::response::active_pricing_resolver(); let summary = finalize_optimization_summary( &handle.optimization_recorder, @@ -326,18 +326,16 @@ impl LlmStreamWrapper { if let Some(event) = event_snapshot && let Some(event) = sanitize_event_with_scope_stack(event, &scope_stack).await { - let _ = subscriber_dispatcher::dispatch_sanitized_event( + let _ = subscriber_dispatcher::dispatch_reserved_sanitized_event( event, Vec::new(), &subscribers, scope_stack.clone(), ); } - if let Some(done) = publication_barrier { - let _ = done.send(()); - } }; - let finalize = subscriber_dispatcher::with_async_publication_context(finalize); + let finalize = + subscriber_dispatcher::with_async_publication_context(publication_barrier, finalize); if background_thread { // `Drop` can run while the current-thread Tokio executor is // synchronously flushing subscribers. Use a dedicated runtime so diff --git a/crates/core/tests/integration/pipeline_tests.rs b/crates/core/tests/integration/pipeline_tests.rs index bff17d6a7..999369412 100644 --- a/crates/core/tests/integration/pipeline_tests.rs +++ b/crates/core/tests/integration/pipeline_tests.rs @@ -30,7 +30,7 @@ use nemo_relay::api::runtime::NemoRelayContextState; use nemo_relay::api::runtime::global_context; use nemo_relay::api::runtime::{LlmExecutionNextFn, LlmJsonStream, LlmStreamExecutionNextFn}; use nemo_relay::api::runtime::{create_scope_stack, set_thread_scope_stack}; -use nemo_relay::api::scope::ScopeType; +use nemo_relay::api::scope::{EmitMarkEventParams, ScopeType, event}; use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber}; use nemo_relay::codec::anthropic::AnthropicMessagesCodec; use nemo_relay::codec::openai_chat::OpenAIChatCodec; @@ -1985,9 +1985,7 @@ async fn test_stream_response_sanitizer_can_flush_subscribers() { 1, Arc::new(|response, _context| { Box::pin(async move { - tokio::task::spawn_blocking(flush_subscribers) - .await - .map_err(|error| FlowError::Internal(error.to_string()))??; + flush_subscribers()?; Ok(Some(response)) }) }), @@ -2018,3 +2016,120 @@ async fn test_stream_response_sanitizer_can_flush_subscribers() { deregister_llm_sanitize_response_guardrail("stream_reentrant_flush_sanitizer").unwrap(); deregister_subscriber("stream_reentrant_flush_subscriber").unwrap(); } + +#[tokio::test] +async fn test_dropped_stream_end_keeps_fifo_position_before_later_mark() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let sanitizer_started = Arc::new(tokio::sync::Notify::new()); + let sanitizer_release = Arc::new(tokio::sync::Notify::new()); + let events = Arc::new(Mutex::new(Vec::new())); + let captured_events = events.clone(); + register_subscriber( + "stream_fifo_subscriber", + Arc::new(move |event| { + captured_events.lock().unwrap().push(event.clone()); + }), + ) + .unwrap(); + register_llm_sanitize_response_guardrail( + "stream_fifo_sanitizer", + 1, + Arc::new({ + let sanitizer_started = sanitizer_started.clone(); + let sanitizer_release = sanitizer_release.clone(); + move |response, _context| { + let sanitizer_started = sanitizer_started.clone(); + let sanitizer_release = sanitizer_release.clone(); + Box::pin(async move { + sanitizer_started.notify_one(); + sanitizer_release.notified().await; + Ok(Some(response)) + }) + } + }), + ) + .unwrap(); + + let stream = llm_stream_call_execute( + LlmStreamCallExecuteParams::builder() + .name("stream_fifo") + .request(make_openai_chat_request("stream me")) + .func(Arc::new(|_| { + Box::pin(async { + assert!(record_llm_optimization_contribution( + routed_model_contribution() + )); + Ok(LlmJsonStream::new(tokio_stream::empty())) + }) + })) + .collector(Box::new(|_chunk| Ok(()))) + .finalizer(Box::new(|| make_openai_chat_response("done"))) + .build(), + ) + .await + .unwrap(); + drop(stream); + + tokio::time::timeout( + std::time::Duration::from_secs(2), + sanitizer_started.notified(), + ) + .await + .expect("stream response sanitizer did not start"); + event( + EmitMarkEventParams::builder() + .name("mark-after-stream-drop") + .build(), + ) + .unwrap(); + + let (flush_done_tx, flush_done_rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let result = flush_subscribers(); + let _ = flush_done_tx.send(result); + }); + let flush_waited_for_end = flush_done_rx + .recv_timeout(std::time::Duration::from_millis(50)) + .is_err(); + sanitizer_release.notify_one(); + assert!( + flush_waited_for_end, + "flush must wait for the pending stream END" + ); + flush_done_rx + .recv_timeout(std::time::Duration::from_secs(2)) + .expect("flush did not finish after sanitizer release") + .unwrap(); + + let events = events.lock().unwrap(); + let end_index = events + .iter() + .position(|event| { + event.name() == "stream_fifo" + && is_scope_event(event, ScopeType::Llm, ScopeCategory::End) + }) + .expect("stream END event"); + let optimization_index = events + .iter() + .position(|event| event.name() == "nemo_relay.llm.optimization") + .expect("stream optimization mark"); + let mark_index = events + .iter() + .position(|event| event.name() == "mark-after-stream-drop") + .expect("later mark event"); + assert!( + optimization_index < end_index, + "optimization marks must retain their position before stream END" + ); + assert!( + end_index < mark_index, + "stream END must retain its FIFO position before the later mark" + ); + + drop(events); + deregister_llm_sanitize_response_guardrail("stream_fifo_sanitizer").unwrap(); + deregister_subscriber("stream_fifo_subscriber").unwrap(); +} diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index c1e6ecdd3..6f10cb2f6 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -3174,10 +3174,10 @@ pub fn deregister_subscriber(name: String) -> Result { /// Return a Promise that resolves when native subscriber callbacks queued /// before this call finish. /// -/// When called from queued publication middleware, or while an asynchronous -/// publication boundary is active, this Promise resolves without waiting. -/// Call it again after the middleware settles to wait for its event and later -/// work. +/// When called from a queued publication sanitizer callback (including event and manual tool/LLM +/// sanitizers), this Promise resolves without waiting to prevent a cycle with the serial +/// dispatcher. Publication middleware must not move such a re-entrant flush to +/// an unmarked worker thread. /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this /// Promise does not block the Node event loop while Promise-returning event sanitizers settle. diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 0e00bab51..53f48f33e 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1524,9 +1524,9 @@ fn deregister_subscriber(name: &str) -> PyResult { /// Wait for subscriber callbacks queued before this call to finish. /// -/// Re-entrant calls and calls observed while an asynchronous publication -/// boundary is active return without waiting. Call again after middleware -/// settles to wait for its event and later work. +/// Public Python wrappers prevent re-entrant event-sanitizer callbacks from +/// waiting on the serial dispatcher. Publication middleware must not move such +/// a re-entrant flush to an unmarked worker thread. #[pyfunction] fn flush_subscribers(py: Python<'_>) -> PyResult<()> { py.detach(core_subscriber_api::flush_subscribers) diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index 1137fc829..aeabaf557 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -95,9 +95,10 @@ def flush() -> None: waiting for observer work. Use this barrier in tests and shutdown paths when captured subscriber output must be complete before continuing. - A re-entrant call, or a call observed while an asynchronous publication - boundary is active, returns without waiting. Call ``flush()`` again after - that middleware settles to wait for its event and later work. + Call this function outside subscriber and queued publication sanitizer + callbacks. A re-entrant call returns without waiting to avoid blocking the + dispatcher. Publication middleware must not move such a call to an unmarked + worker thread. """ if _event_sanitizer_callback_active(): return None From fd32193ac09bfdb9e2a0987e0cb7ce2056848795 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 28 Jul 2026 23:53:35 -0400 Subject: [PATCH 39/83] fix: address async middleware review feedback Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 3 + crates/node/src/api/mod.rs | 110 +++++++++++++----- crates/node/tests/event_sanitizers_tests.mjs | 4 +- crates/node/tests/llm_tests.mjs | 6 +- crates/plugin/README.md | 2 +- .../tests/coverage/py_api_coverage_tests.rs | 11 +- .../coverage/py_callable_coverage_tests.rs | 104 ++++++++--------- docs/reference/migration-guides.mdx | 4 + python/tests/test_llm.py | 4 +- 9 files changed, 158 insertions(+), 90 deletions(-) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index fc77a9bcc..fa6e22278 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -171,6 +171,9 @@ mod native { subscribers: &[EventSubscriberFn], scope_stack: ScopeStackHandle, ) -> bool { + if subscribers.is_empty() { + return true; + } let message = DispatcherMessage::Deliver { event: Box::new(event), transform: Some(transform), diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 6f10cb2f6..efc8a6b28 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -461,9 +461,15 @@ pub fn end_stream(stream_id: f64) { #[allow(clippy::enum_variant_names)] #[derive(Clone, Debug, Eq, Hash, PartialEq)] enum PromiseAwareKey { + GlobalMarkSanitize(String), + GlobalScopeStartSanitize(String), + GlobalScopeEndSanitize(String), GlobalToolExecution(String), GlobalLlmExecution(String), GlobalLlmStreamExecution(String), + ScopeMarkSanitize { scope_uuid: String, name: String }, + ScopeStartSanitize { scope_uuid: String, name: String }, + ScopeEndSanitize { scope_uuid: String, name: String }, ScopeToolExecution { scope_uuid: String, name: String }, ScopeLlmExecution { scope_uuid: String, name: String }, ScopeLlmStreamExecution { scope_uuid: String, name: String }, @@ -472,10 +478,16 @@ enum PromiseAwareKey { impl PromiseAwareKey { fn scope_uuid(&self) -> Option<&str> { match self { - Self::ScopeToolExecution { scope_uuid, .. } + Self::ScopeMarkSanitize { scope_uuid, .. } + | Self::ScopeStartSanitize { scope_uuid, .. } + | Self::ScopeEndSanitize { scope_uuid, .. } + | Self::ScopeToolExecution { scope_uuid, .. } | Self::ScopeLlmExecution { scope_uuid, .. } | Self::ScopeLlmStreamExecution { scope_uuid, .. } => Some(scope_uuid), - Self::GlobalToolExecution(_) + Self::GlobalMarkSanitize(_) + | Self::GlobalScopeStartSanitize(_) + | Self::GlobalScopeEndSanitize(_) + | Self::GlobalToolExecution(_) | Self::GlobalLlmExecution(_) | Self::GlobalLlmStreamExecution(_) => None, } @@ -646,18 +658,20 @@ fn add_plugin_event_sanitizer( let name = format!("{}{}", namespace_prefix, ctx.get::(0)?); let priority = ctx.get::(1)?; let callback = ctx.get::(2)?; - register(&name, priority, node_event_sanitize_fn(ctx.env, &callback)?) - .map_err(to_napi_err)?; + let (callback, promise_aware) = node_event_sanitize_fn(ctx.env, &callback)?; + register(&name, priority, callback).map_err(to_napi_err)?; let name_clone = name.clone(); registrations.lock().unwrap().push(PluginRegistration::new( "plugin", name_clone.clone(), Box::new(move || { - deregister(&name_clone).map(|_| ()).map_err(|error| { + let result = deregister(&name_clone).map(|_| ()).map_err(|error| { PluginError::RegistrationFailed(format!( "{label} deregistration failed: {error}" )) - }) + }); + promise_aware.close(); + result }), )); ctx.env.get_undefined() @@ -1412,11 +1426,17 @@ impl PersistentJsFunction { } } -fn node_event_sanitize_fn(env: &Env, func: &JsFunction) -> napi::Result { +fn node_event_sanitize_fn( + env: &Env, + func: &JsFunction, +) -> napi::Result<(EventSanitizeFn, Arc)> { let callback = Arc::new(crate::promise_call::PromiseAwareFn::new_event_sanitizer( env, func, )?); - Ok(callable::wrap_js_event_sanitize_promise_fn(callback)) + Ok(( + callable::wrap_js_event_sanitize_promise_fn(callback.clone()), + callback, + )) } type NodeLlmCodec = ( @@ -2663,7 +2683,13 @@ pub fn llm_stream_call_execute( // --------------------------------------------------------------------------- macro_rules! napi_event_guardrail_api { - ($register_name:ident, $deregister_name:ident, $core_register:path, $core_deregister:path) => { + ( + $register_name:ident, + $deregister_name:ident, + $core_register:path, + $core_deregister:path, + $key:ident + ) => { /// Register an event sanitize guardrail. /// /// The callback may return fields directly or in a Promise. Scope and mark @@ -2681,13 +2707,20 @@ macro_rules! napi_event_guardrail_api { )] guardrail: JsFunction, ) -> Result<()> { - $core_register(&name, priority, node_event_sanitize_fn(&env, &guardrail)?) - .map_err(to_napi_err) + let (callback, promise_aware) = node_event_sanitize_fn(&env, &guardrail)?; + $core_register(&name, priority, callback).map_err(to_napi_err)?; + remember_promise_aware(PromiseAwareKey::$key(name), promise_aware); + Ok(()) } #[napi] pub fn $deregister_name(name: String) -> Result { - $core_deregister(&name).map_err(to_napi_err) + let key = PromiseAwareKey::$key(name.clone()); + let removed = $core_deregister(&name).map_err(to_napi_err)?; + if removed { + forget_promise_aware(&key); + } + Ok(removed) } }; } @@ -2696,19 +2729,22 @@ napi_event_guardrail_api!( register_mark_sanitize_guardrail, deregister_mark_sanitize_guardrail, core_registry_api::register_mark_sanitize_guardrail, - core_registry_api::deregister_mark_sanitize_guardrail + core_registry_api::deregister_mark_sanitize_guardrail, + GlobalMarkSanitize ); napi_event_guardrail_api!( register_scope_sanitize_start_guardrail, deregister_scope_sanitize_start_guardrail, core_registry_api::register_scope_sanitize_start_guardrail, - core_registry_api::deregister_scope_sanitize_start_guardrail + core_registry_api::deregister_scope_sanitize_start_guardrail, + GlobalScopeStartSanitize ); napi_event_guardrail_api!( register_scope_sanitize_end_guardrail, deregister_scope_sanitize_end_guardrail, core_registry_api::register_scope_sanitize_end_guardrail, - core_registry_api::deregister_scope_sanitize_end_guardrail + core_registry_api::deregister_scope_sanitize_end_guardrail, + GlobalScopeEndSanitize ); macro_rules! napi_guardrail_tool_api { @@ -3206,7 +3242,13 @@ pub fn flush_subscribers(env: Env) -> Result { // --------------------------------------------------------------------------- macro_rules! napi_scope_event_guardrail_api { - ($register_name:ident, $deregister_name:ident, $core_register:path, $core_deregister:path) => { + ( + $register_name:ident, + $deregister_name:ident, + $core_register:path, + $core_deregister:path, + $key:ident + ) => { /// Register a scope-local event sanitize guardrail. /// /// The callback may return fields directly or in a Promise. Scope and mark @@ -3227,20 +3269,31 @@ macro_rules! napi_scope_event_guardrail_api { ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - $core_register( - &uuid, - &name, - priority, - node_event_sanitize_fn(&env, &guardrail)?, - ) - .map_err(to_napi_err) + let (callback, promise_aware) = node_event_sanitize_fn(&env, &guardrail)?; + $core_register(&uuid, &name, priority, callback).map_err(to_napi_err)?; + remember_promise_aware( + PromiseAwareKey::$key { + scope_uuid, + name, + }, + promise_aware, + ); + Ok(()) } #[napi] pub fn $deregister_name(scope_uuid: String, name: String) -> Result { + let key = PromiseAwareKey::$key { + scope_uuid: scope_uuid.clone(), + name: name.clone(), + }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - $core_deregister(&uuid, &name).map_err(to_napi_err) + let removed = $core_deregister(&uuid, &name).map_err(to_napi_err)?; + if removed { + forget_promise_aware(&key); + } + Ok(removed) } }; } @@ -3249,19 +3302,22 @@ napi_scope_event_guardrail_api!( scope_register_mark_sanitize_guardrail, scope_deregister_mark_sanitize_guardrail, core_registry_api::scope_register_mark_sanitize_guardrail, - core_registry_api::scope_deregister_mark_sanitize_guardrail + core_registry_api::scope_deregister_mark_sanitize_guardrail, + ScopeMarkSanitize ); napi_scope_event_guardrail_api!( scope_register_scope_sanitize_start_guardrail, scope_deregister_scope_sanitize_start_guardrail, core_registry_api::scope_register_scope_sanitize_start_guardrail, - core_registry_api::scope_deregister_scope_sanitize_start_guardrail + core_registry_api::scope_deregister_scope_sanitize_start_guardrail, + ScopeStartSanitize ); napi_scope_event_guardrail_api!( scope_register_scope_sanitize_end_guardrail, scope_deregister_scope_sanitize_end_guardrail, core_registry_api::scope_register_scope_sanitize_end_guardrail, - core_registry_api::scope_deregister_scope_sanitize_end_guardrail + core_registry_api::scope_deregister_scope_sanitize_end_guardrail, + ScopeEndSanitize ); macro_rules! napi_scope_guardrail_tool_api { diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 2380f2cbc..3963c7a2c 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -204,8 +204,8 @@ describe('event sanitizer registries', () => { if (event.name === 'descendant-flush-origin') { setTimeout(async () => { await secondEntered; - descendantFlushStarted(); lib.flushSubscribers().then(descendantFlush.resolve, descendantFlush.reject); + descendantFlushStarted(); }, 0); } else if (event.name === 'descendant-flush-blocked') { secondSanitizerEntered(); @@ -220,7 +220,7 @@ describe('event sanitizer registries', () => { await flushStarted; const state = await Promise.race([ flushed.then(() => 'flushed'), - new Promise((resolve) => setImmediate(() => resolve('pending'))), + new Promise((resolve) => setTimeout(() => resolve('pending'), 50)), ]); assert.equal(state, 'pending'); releaseSecondSanitizer(); diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index c2bad301d..25fdccf63 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -696,7 +696,11 @@ describe('LLM guardrails', () => { try { const handle = llmCall('node_manual_flush', makeNative()); llmCallEnd(handle, { response: 'ok' }); - await flushSubscribers(); + const outcome = await Promise.race([ + flushSubscribers().then(() => 'flushed'), + new Promise((resolve) => setTimeout(() => resolve('timeout'), 2000)), + ]); + assert.equal(outcome, 'flushed', 'flushSubscribers deadlocked inside an async sanitizer'); } finally { deregisterLlmSanitizeRequestGuardrail('node_manual_flush_request'); deregisterLlmSanitizeResponseGuardrail('node_manual_flush_response'); diff --git a/crates/plugin/README.md b/crates/plugin/README.md index ecafb951f..8df78823f 100644 --- a/crates/plugin/README.md +++ b/crates/plugin/README.md @@ -45,7 +45,7 @@ the dynamic-library boundary on the stable C-compatible ABI. - **Stable native ABI v3**: C-compatible host and plugin tables behind the safe Rust authoring interface. The v3 tables preserve a v2-compatible field prefix, but native plugins must still be rebuilt for v3 as described in the - [0.7 migration guide](../../docs/reference/migration-guides.mdx#upgrade-to-nemo-relay-07). + [0.7 migration guide](https://docs.nvidia.com/nemo/relay/reference/migration-guides#upgrade-to-nemo-relay-07). - **Raw async middleware**: Completion-based raw registrations for plugins that need asynchronous guardrails, intercepts, or event sanitizers. Typed Rust callbacks remain synchronous convenience APIs. diff --git a/crates/python/tests/coverage/py_api_coverage_tests.rs b/crates/python/tests/coverage/py_api_coverage_tests.rs index 5c5d35def..45df22794 100644 --- a/crates/python/tests/coverage/py_api_coverage_tests.rs +++ b/crates/python/tests/coverage/py_api_coverage_tests.rs @@ -329,10 +329,12 @@ async def run_llm(api, request, func, handle, attributes, codec, response_codec) async def run_standalone(api, request): tool_args = await api.tool_request_intercepts("demo-tool", {"value": 1}) await api.tool_conditional_execution("demo-tool", tool_args) + conditional_allowed = True llm_outcome = await api.llm_request_intercepts("demo-llm", request) await api.llm_conditional_execution(llm_outcome.request) return { "tool_value": tool_args["value"], + "conditional_allowed": conditional_allowed, "llm_header": llm_outcome.request.headers["x-intercepted"], } @@ -528,10 +530,6 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute .to_string() .contains("requires an async caller") ); - assert!( - deregister_tool_conditional_execution_guardrail(&async_sync_rejection_name).unwrap() - ); - let llm_request = PyLLMRequest { inner: nemo_relay::api::llm::LlmRequest { headers: serde_json::Map::new(), @@ -579,7 +577,7 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute .unwrap(); assert_eq!( crate::convert::py_to_json(&standalone).unwrap(), - json!({"tool_value": 3, "llm_header": "1"}) + json!({"tool_value": 3, "conditional_allowed": true, "llm_header": "1"}) ); let tool_result = event_loop @@ -666,6 +664,9 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute json!([{"delta": 11}, {"delta": 12}]) ); }); + assert!( + deregister_tool_conditional_execution_guardrail(&async_sync_rejection_name).unwrap() + ); let events = helpers.getattr("events").unwrap(); let events_json = crate::convert::py_to_json(events.as_any()).unwrap(); diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index d8ece09e7..d50760803 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -743,11 +743,12 @@ fn event_sanitize_wrapper_covers_conversion_success_and_error_propagation() { use nemo_relay::api::event::{BaseEvent, MarkEvent}; let _python = crate::test_support::init_python_test(); - Python::attach(|py| { - let _context_module = install_event_sanitizer_context_module(py); - let module = load_module( - py, - r#" + let (_context_module, sanitized_fn, async_sanitizer, raises_fn, invalid_fn) = + Python::attach(|py| { + let context_module = install_event_sanitizer_context_module(py); + let module = load_module( + py, + r#" import asyncio def sanitize(event, fields): @@ -767,55 +768,54 @@ def raises(event, fields): def invalid(event, fields): return "not fields" "#, - ); - let event = Event::Mark(MarkEvent::new( - BaseEvent::builder().name("checkpoint").build(), - None, - None, - )); - let fields = EventSanitizeFields { - data: Some(json!({"secret": true})), - category_profile: None, - metadata: Some(json!({"secret": true})), - }; - - let runtime = tokio::runtime::Runtime::new().unwrap(); - let sanitized = runtime - .block_on(wrap_py_event_sanitize_fn( - module.getattr("sanitize").unwrap().unbind(), - )(Arc::new(event.clone()), fields.clone())) - .unwrap(); - assert_eq!(sanitized.data, Some(json!({"safe": "checkpoint"}))); - assert_eq!(sanitized.metadata, None); + ); + ( + context_module, + wrap_py_event_sanitize_fn(module.getattr("sanitize").unwrap().unbind()), + wrap_py_event_sanitize_fn(module.getattr("async_sanitize").unwrap().unbind()), + wrap_py_event_sanitize_fn(module.getattr("raises").unwrap().unbind()), + wrap_py_event_sanitize_fn(module.getattr("invalid").unwrap().unbind()), + ) + }); + let event = Event::Mark(MarkEvent::new( + BaseEvent::builder().name("checkpoint").build(), + None, + None, + )); + let fields = EventSanitizeFields { + data: Some(json!({"secret": true})), + category_profile: None, + metadata: Some(json!({"secret": true})), + }; - let async_sanitized = runtime - .block_on(wrap_py_event_sanitize_fn( - module.getattr("async_sanitize").unwrap().unbind(), - )(Arc::new(event.clone()), fields.clone())) - .unwrap(); - assert_eq!( - async_sanitized.data, - Some(json!({"async_safe": "checkpoint"})) - ); + let runtime = tokio::runtime::Runtime::new().unwrap(); + let sanitized = runtime + .block_on(sanitized_fn(Arc::new(event.clone()), fields.clone())) + .unwrap(); + assert_eq!(sanitized.data, Some(json!({"safe": "checkpoint"}))); + assert_eq!(sanitized.metadata, None); - let raised = runtime - .block_on(wrap_py_event_sanitize_fn( - module.getattr("raises").unwrap().unbind(), - )(Arc::new(event.clone()), fields.clone())) - .unwrap_err(); - assert!(raised.to_string().contains("sanitize boom")); - - let invalid = runtime - .block_on(wrap_py_event_sanitize_fn( - module.getattr("invalid").unwrap().unbind(), - )(Arc::new(event), fields.clone())) - .unwrap_err(); - assert!( - invalid - .to_string() - .contains("invalid event sanitizer result") - ); - }); + let async_sanitized = runtime + .block_on(async_sanitizer(Arc::new(event.clone()), fields.clone())) + .unwrap(); + assert_eq!( + async_sanitized.data, + Some(json!({"async_safe": "checkpoint"})) + ); + + let raised = runtime + .block_on(raises_fn(Arc::new(event.clone()), fields.clone())) + .unwrap_err(); + assert!(raised.to_string().contains("sanitize boom")); + + let invalid = runtime + .block_on(invalid_fn(Arc::new(event), fields)) + .unwrap_err(); + assert!( + invalid + .to_string() + .contains("invalid event sanitizer result") + ); } #[test] diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index 4fc5529e0..864096995 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -72,6 +72,10 @@ Python and Node.js registration names are unchanged. Mark a Python callback `async def`, or return a Promise from Node.js, only when it needs asynchronous work; existing direct-value callbacks remain supported. +Queued Python event sanitizers registered outside a running event loop use +`asyncio.run` with a fresh loop. Bare coroutine results are supported in that +fallback; `Task` and `Future` instances bound to a different loop are not. + Scope, mark, and manual tool/LLM lifecycle APIs remain synchronous. This includes `push_scope`, `pop_scope`, mark APIs, `tool_call`, `tool_call_end`, `llm_call`, and `llm_call_end`. These APIs snapshot the event and visible diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index 22bc1aa18..5bf315f3e 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -183,7 +183,7 @@ async def test_manual_async_sanitizers_can_flush_subscribers(self): request_flushed = False response_flushed = False - async def sanitize_request(request, context): + async def sanitize_request(request, context) -> LLMRequest: nonlocal request_flushed del context await asyncio.sleep(0) @@ -191,7 +191,7 @@ async def sanitize_request(request, context): request_flushed = True return request - async def sanitize_response(response, context): + async def sanitize_response(response, context) -> dict: nonlocal response_flushed del context await asyncio.sleep(0) From 453b2fd0748c478c63e36a8df9cc7623b09d96dc Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 00:07:51 -0400 Subject: [PATCH 40/83] fix: preserve async binding publication progress Signed-off-by: Will Killian --- crates/node/src/api/mod.rs | 114 +++++------------- crates/node/tests/event_sanitizers_tests.mjs | 34 ++++++ crates/python/src/py_callable.rs | 29 ++++- .../about-nemo-relay/concepts/subscribers.mdx | 3 +- docs/reference/event-sanitizers.mdx | 5 + docs/reference/migration-guides.mdx | 20 ++- python/nemo_relay/README.md | 2 + python/nemo_relay/subscribers.py | 29 ++++- python/tests/test_event_sanitizers.py | 47 ++++++++ 9 files changed, 189 insertions(+), 94 deletions(-) diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index efc8a6b28..048429c47 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -461,15 +461,9 @@ pub fn end_stream(stream_id: f64) { #[allow(clippy::enum_variant_names)] #[derive(Clone, Debug, Eq, Hash, PartialEq)] enum PromiseAwareKey { - GlobalMarkSanitize(String), - GlobalScopeStartSanitize(String), - GlobalScopeEndSanitize(String), GlobalToolExecution(String), GlobalLlmExecution(String), GlobalLlmStreamExecution(String), - ScopeMarkSanitize { scope_uuid: String, name: String }, - ScopeStartSanitize { scope_uuid: String, name: String }, - ScopeEndSanitize { scope_uuid: String, name: String }, ScopeToolExecution { scope_uuid: String, name: String }, ScopeLlmExecution { scope_uuid: String, name: String }, ScopeLlmStreamExecution { scope_uuid: String, name: String }, @@ -478,16 +472,10 @@ enum PromiseAwareKey { impl PromiseAwareKey { fn scope_uuid(&self) -> Option<&str> { match self { - Self::ScopeMarkSanitize { scope_uuid, .. } - | Self::ScopeStartSanitize { scope_uuid, .. } - | Self::ScopeEndSanitize { scope_uuid, .. } - | Self::ScopeToolExecution { scope_uuid, .. } + Self::ScopeToolExecution { scope_uuid, .. } | Self::ScopeLlmExecution { scope_uuid, .. } | Self::ScopeLlmStreamExecution { scope_uuid, .. } => Some(scope_uuid), - Self::GlobalMarkSanitize(_) - | Self::GlobalScopeStartSanitize(_) - | Self::GlobalScopeEndSanitize(_) - | Self::GlobalToolExecution(_) + Self::GlobalToolExecution(_) | Self::GlobalLlmExecution(_) | Self::GlobalLlmStreamExecution(_) => None, } @@ -658,20 +646,18 @@ fn add_plugin_event_sanitizer( let name = format!("{}{}", namespace_prefix, ctx.get::(0)?); let priority = ctx.get::(1)?; let callback = ctx.get::(2)?; - let (callback, promise_aware) = node_event_sanitize_fn(ctx.env, &callback)?; - register(&name, priority, callback).map_err(to_napi_err)?; + register(&name, priority, node_event_sanitize_fn(ctx.env, &callback)?) + .map_err(to_napi_err)?; let name_clone = name.clone(); registrations.lock().unwrap().push(PluginRegistration::new( "plugin", name_clone.clone(), Box::new(move || { - let result = deregister(&name_clone).map(|_| ()).map_err(|error| { + deregister(&name_clone).map(|_| ()).map_err(|error| { PluginError::RegistrationFailed(format!( "{label} deregistration failed: {error}" )) - }); - promise_aware.close(); - result + }) }), )); ctx.env.get_undefined() @@ -1426,17 +1412,15 @@ impl PersistentJsFunction { } } -fn node_event_sanitize_fn( - env: &Env, - func: &JsFunction, -) -> napi::Result<(EventSanitizeFn, Arc)> { +fn node_event_sanitize_fn(env: &Env, func: &JsFunction) -> napi::Result { + // The registry and queued snapshots own the only callback references. + // PromiseAwareFn releases its TSFN on the last drop, so deregistration + // preserves already-snapshotted publication while still cleaning up + // deterministically once that work finishes. let callback = Arc::new(crate::promise_call::PromiseAwareFn::new_event_sanitizer( env, func, )?); - Ok(( - callable::wrap_js_event_sanitize_promise_fn(callback.clone()), - callback, - )) + Ok(callable::wrap_js_event_sanitize_promise_fn(callback)) } type NodeLlmCodec = ( @@ -2683,13 +2667,7 @@ pub fn llm_stream_call_execute( // --------------------------------------------------------------------------- macro_rules! napi_event_guardrail_api { - ( - $register_name:ident, - $deregister_name:ident, - $core_register:path, - $core_deregister:path, - $key:ident - ) => { + ($register_name:ident, $deregister_name:ident, $core_register:path, $core_deregister:path) => { /// Register an event sanitize guardrail. /// /// The callback may return fields directly or in a Promise. Scope and mark @@ -2707,20 +2685,13 @@ macro_rules! napi_event_guardrail_api { )] guardrail: JsFunction, ) -> Result<()> { - let (callback, promise_aware) = node_event_sanitize_fn(&env, &guardrail)?; - $core_register(&name, priority, callback).map_err(to_napi_err)?; - remember_promise_aware(PromiseAwareKey::$key(name), promise_aware); - Ok(()) + $core_register(&name, priority, node_event_sanitize_fn(&env, &guardrail)?) + .map_err(to_napi_err) } #[napi] pub fn $deregister_name(name: String) -> Result { - let key = PromiseAwareKey::$key(name.clone()); - let removed = $core_deregister(&name).map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + $core_deregister(&name).map_err(to_napi_err) } }; } @@ -2729,22 +2700,19 @@ napi_event_guardrail_api!( register_mark_sanitize_guardrail, deregister_mark_sanitize_guardrail, core_registry_api::register_mark_sanitize_guardrail, - core_registry_api::deregister_mark_sanitize_guardrail, - GlobalMarkSanitize + core_registry_api::deregister_mark_sanitize_guardrail ); napi_event_guardrail_api!( register_scope_sanitize_start_guardrail, deregister_scope_sanitize_start_guardrail, core_registry_api::register_scope_sanitize_start_guardrail, - core_registry_api::deregister_scope_sanitize_start_guardrail, - GlobalScopeStartSanitize + core_registry_api::deregister_scope_sanitize_start_guardrail ); napi_event_guardrail_api!( register_scope_sanitize_end_guardrail, deregister_scope_sanitize_end_guardrail, core_registry_api::register_scope_sanitize_end_guardrail, - core_registry_api::deregister_scope_sanitize_end_guardrail, - GlobalScopeEndSanitize + core_registry_api::deregister_scope_sanitize_end_guardrail ); macro_rules! napi_guardrail_tool_api { @@ -3242,13 +3210,7 @@ pub fn flush_subscribers(env: Env) -> Result { // --------------------------------------------------------------------------- macro_rules! napi_scope_event_guardrail_api { - ( - $register_name:ident, - $deregister_name:ident, - $core_register:path, - $core_deregister:path, - $key:ident - ) => { + ($register_name:ident, $deregister_name:ident, $core_register:path, $core_deregister:path) => { /// Register a scope-local event sanitize guardrail. /// /// The callback may return fields directly or in a Promise. Scope and mark @@ -3269,31 +3231,20 @@ macro_rules! napi_scope_event_guardrail_api { ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let (callback, promise_aware) = node_event_sanitize_fn(&env, &guardrail)?; - $core_register(&uuid, &name, priority, callback).map_err(to_napi_err)?; - remember_promise_aware( - PromiseAwareKey::$key { - scope_uuid, - name, - }, - promise_aware, - ); - Ok(()) + $core_register( + &uuid, + &name, + priority, + node_event_sanitize_fn(&env, &guardrail)?, + ) + .map_err(to_napi_err) } #[napi] pub fn $deregister_name(scope_uuid: String, name: String) -> Result { - let key = PromiseAwareKey::$key { - scope_uuid: scope_uuid.clone(), - name: name.clone(), - }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let removed = $core_deregister(&uuid, &name).map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + $core_deregister(&uuid, &name).map_err(to_napi_err) } }; } @@ -3302,22 +3253,19 @@ napi_scope_event_guardrail_api!( scope_register_mark_sanitize_guardrail, scope_deregister_mark_sanitize_guardrail, core_registry_api::scope_register_mark_sanitize_guardrail, - core_registry_api::scope_deregister_mark_sanitize_guardrail, - ScopeMarkSanitize + core_registry_api::scope_deregister_mark_sanitize_guardrail ); napi_scope_event_guardrail_api!( scope_register_scope_sanitize_start_guardrail, scope_deregister_scope_sanitize_start_guardrail, core_registry_api::scope_register_scope_sanitize_start_guardrail, - core_registry_api::scope_deregister_scope_sanitize_start_guardrail, - ScopeStartSanitize + core_registry_api::scope_deregister_scope_sanitize_start_guardrail ); napi_scope_event_guardrail_api!( scope_register_scope_sanitize_end_guardrail, scope_deregister_scope_sanitize_end_guardrail, core_registry_api::scope_register_scope_sanitize_end_guardrail, - core_registry_api::scope_deregister_scope_sanitize_end_guardrail, - ScopeEndSanitize + core_registry_api::scope_deregister_scope_sanitize_end_guardrail ); macro_rules! napi_scope_guardrail_tool_api { diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 3963c7a2c..0baf9ea8c 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -129,6 +129,40 @@ describe('event sanitizer registries', () => { assert.deepEqual(events.at(-1).data, { sanitized: true }); }); + it('preserves snapshotted sanitizers after deregistration', async () => { + const events = capture('node-event-sanitize-snapshot-sub'); + let blockerEntered; + const entered = new Promise((resolve) => { + blockerEntered = resolve; + }); + let releaseBlocker; + const release = new Promise((resolve) => { + releaseBlocker = resolve; + }); + lib.registerMarkSanitizeGuardrail('node-event-snapshot-blocker', 0, async (_event, fields) => { + blockerEntered(); + await release; + return fields; + }); + lib.registerMarkSanitizeGuardrail('node-event-snapshot-target', 10, async (_event, fields) => { + return { ...fields, data: { snapshotted: true } }; + }); + try { + lib.event('snapshot-checkpoint', null, { raw: true }); + await entered; + assert.equal(lib.deregisterMarkSanitizeGuardrail('node-event-snapshot-target'), true); + releaseBlocker(); + await lib.flushSubscribers(); + await waitFor(events, 1); + } finally { + releaseBlocker(); + lib.deregisterMarkSanitizeGuardrail('node-event-snapshot-blocker'); + lib.deregisterMarkSanitizeGuardrail('node-event-snapshot-target'); + lib.deregisterSubscriber('node-event-sanitize-snapshot-sub'); + } + assert.deepEqual(events.at(-1).data, { snapshotted: true }); + }); + it('does not deadlock when an async sanitizer flushes subscribers', async () => { const events = capture('node-event-sanitize-reentrant-flush-sub'); let flushReturned = false; diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 279040388..a947d628b 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -179,6 +179,25 @@ fn capture_python_task_locals() -> Option { Python::attach(|py| pyo3_async_runtimes::tokio::get_current_locals(py).ok()) } +fn task_locals_with_running_loop(registered: Option<&TaskLocals>) -> Option { + capture_python_task_locals() + .or_else(|| registered.cloned()) + .filter(|locals| { + Python::attach(|py| { + let event_loop = locals.event_loop(py); + let running = event_loop + .call_method0("is_running") + .and_then(|value| value.extract::()) + .unwrap_or(false); + let closed = event_loop + .call_method0("is_closed") + .and_then(|value| value.extract::()) + .unwrap_or(true); + running && !closed + }) + }) +} + async fn resolve_py_object_or_future( outcome: FlowResult, PyValueFuture>>, ) -> FlowResult> { @@ -472,7 +491,7 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { let task_locals = capture_python_task_locals(); Arc::new(move |name: String, args: Json| { let py_fn = py_fn.clone(); - let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { @@ -890,7 +909,7 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest Arc::new( move |request: LlmRequest, context: LlmSanitizeRequestContext| { let py_fn = py_fn.clone(); - let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { @@ -935,7 +954,7 @@ pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { let task_locals = capture_python_task_locals(); Arc::new(move |request: LlmRequest| { let py_fn = py_fn.clone(); - let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let result = py_fn @@ -1113,7 +1132,7 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon let task_locals = capture_python_task_locals(); Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { let py_fn = py_fn.clone(); - let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { @@ -1187,7 +1206,7 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let task_locals = capture_python_task_locals(); Arc::new(move |event: Arc, fields: EventSanitizeFields| { let py_fn = py_fn.clone(); - let task_locals = capture_python_task_locals().or_else(|| task_locals.clone()); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { diff --git a/docs/about-nemo-relay/concepts/subscribers.mdx b/docs/about-nemo-relay/concepts/subscribers.mdx index 8ac477ca2..15a205d17 100644 --- a/docs/about-nemo-relay/concepts/subscribers.mdx +++ b/docs/about-nemo-relay/concepts/subscribers.mdx @@ -149,7 +149,8 @@ Use the subscriber flush API when application shutdown, tests, or examples must observe side effects from callbacks that were already queued before the barrier: - Rust: `nemo_relay::api::subscriber::flush_subscribers()?` -- Python: `nemo_relay.subscribers.flush()` +- Python: `nemo_relay.subscribers.flush()` from synchronous code, or + `await nemo_relay.subscribers.flush_async()` from an `asyncio` task - Node.js: `flushSubscribers()`, then await an event-loop tick for JavaScript callback side effects - FFI: `nemo_relay_flush_subscribers()` diff --git a/docs/reference/event-sanitizers.mdx b/docs/reference/event-sanitizers.mdx index 9ebeb5eae..ea69692fd 100644 --- a/docs/reference/event-sanitizers.mdx +++ b/docs/reference/event-sanitizers.mdx @@ -69,6 +69,11 @@ This preserves FIFO start/end/mark delivery without making `push_scope`, event is emitted still applies to its queued snapshot. An asynchronous sanitizer rejection fails open and preserves the last valid event fields. +When Python code emits events from an `asyncio` task, await +`nemo_relay.subscribers.flush_async()` to drain queued publication without +blocking the event loop that runs async sanitizers. The synchronous +`subscribers.flush()` barrier raises when called from a running event loop. + ## Registration Lifetimes Where you register a sanitizer determines how long it stays active. diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index 864096995..cb09524e6 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -68,13 +68,27 @@ register_tool_conditional_execution_guardrail( )?; ``` +Rust middleware inputs that previously borrowed call data are now owned so +they can live for the duration of the returned future. Update explicitly typed +closures and named callback functions as follows: + +- Event sanitizers receive `Arc` instead of `&Event`. +- Tool sanitizers, conditional guardrails, and request intercepts receive an + owned `String` tool name; tool conditional guardrails also receive an owned + `Json` payload instead of `&Json`. +- LLM conditional guardrails receive an owned `LlmRequest` instead of + `&LlmRequest`, and LLM request intercepts receive an owned `String` call + name. + Python and Node.js registration names are unchanged. Mark a Python callback `async def`, or return a Promise from Node.js, only when it needs asynchronous work; existing direct-value callbacks remain supported. -Queued Python event sanitizers registered outside a running event loop use -`asyncio.run` with a fresh loop. Bare coroutine results are supported in that -fallback; `Task` and `Future` instances bound to a different loop are not. +When queued Python middleware has no live captured event loop—including when +it was registered outside a loop or its registration loop has closed—the +fallback uses `asyncio.run` with a fresh loop. Bare coroutine results are +supported in that fallback; `Task` and `Future` instances bound to a different +loop are not. Scope, mark, and manual tool/LLM lifecycle APIs remain synchronous. This includes `push_scope`, `pop_scope`, mark APIs, `tool_call`, `tool_call_end`, diff --git a/python/nemo_relay/README.md b/python/nemo_relay/README.md index d357e8ed6..8d1283d4c 100644 --- a/python/nemo_relay/README.md +++ b/python/nemo_relay/README.md @@ -153,6 +153,8 @@ nemo_relay.subscribers.deregister("printer") Native subscriber delivery is asynchronous, so call `nemo_relay.subscribers.flush()` before you read subscriber output or exit. +From an `asyncio` task, use `await nemo_relay.subscribers.flush_async()` so +async event sanitizers can continue running on that event loop. For host integrations that need a serialized event shape, consume the canonical JSON payload from the subscriber event object: diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index aeabaf557..9762d3ab6 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -22,6 +22,7 @@ def log_event(event): nemo_relay.subscribers.deregister("logger") """ +import asyncio from collections.abc import Callable from typing import TYPE_CHECKING @@ -98,11 +99,35 @@ def flush() -> None: Call this function outside subscriber and queued publication sanitizer callbacks. A re-entrant call returns without waiting to avoid blocking the dispatcher. Publication middleware must not move such a call to an unmarked - worker thread. + worker thread. From an ``asyncio`` task, await :func:`flush_async` instead. + + Raises: + RuntimeError: If called while an ``asyncio`` event loop is running on + the current thread. """ if _event_sanitizer_callback_active(): return None + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + raise RuntimeError( + "subscribers.flush() cannot block a running asyncio event loop; use 'await subscribers.flush_async()'" + ) return _native_flush() -__all__ = ["deregister", "flush", "register"] +async def flush_async() -> None: + """Wait asynchronously for subscriber callbacks already queued by Relay. + + Use this barrier from an ``asyncio`` task. The blocking native wait runs on + a worker thread so an event sanitizer scheduled on the caller's event loop + can continue to make progress. + """ + if _event_sanitizer_callback_active(): + return None + await asyncio.to_thread(_native_flush) + + +__all__ = ["deregister", "flush", "flush_async", "register"] diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index c0dc3c78e..b002972de 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -97,6 +97,53 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert events[-1].data == {"async": True} +async def test_async_flush_keeps_originating_sanitizer_loop_running(capture_events): + _capture_name, events = capture_events + + async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + await asyncio.sleep(0) + return { + "data": {"async_flush": True}, + "category_profile": fields["category_profile"], + "metadata": fields["metadata"], + } + + guardrails.register_mark_sanitize("python-async-flush", 0, sanitize) + try: + scope.event("async-flush-checkpoint", data={"raw": True}) + with pytest.raises(RuntimeError, match=r"await subscribers\.flush_async"): + subscribers.flush() + await asyncio.wait_for(subscribers.flush_async(), timeout=2) + finally: + guardrails.deregister_mark_sanitize("python-async-flush") + + assert events[-1].data == {"async_flush": True} + + +def test_async_sanitizer_registered_on_closed_loop_uses_fallback(capture_events): + _capture_name, events = capture_events + + async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + await asyncio.sleep(0) + return { + "data": {"fresh_loop": True}, + "category_profile": fields["category_profile"], + "metadata": fields["metadata"], + } + + async def register() -> None: + guardrails.register_mark_sanitize("python-closed-loop-fallback", 0, sanitize) + + asyncio.run(register()) + try: + scope.event("closed-loop-checkpoint", data={"raw": True}) + subscribers.flush() + finally: + guardrails.deregister_mark_sanitize("python-closed-loop-fallback") + + assert events[-1].data == {"fresh_loop": True} + + @pytest.mark.parametrize("asynchronous", [False, True]) async def test_event_sanitizer_flush_is_reentrant(capture_events, asynchronous): _capture_name, events = capture_events From 6243d6416e327e32cce2bc1f5960137bfaf5bc41 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 00:33:15 -0400 Subject: [PATCH 41/83] fix: complete async middleware review remediation Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 3 - crates/core/src/api/scope.rs | 46 +- crates/core/src/api/shared.rs | 2 +- crates/core/src/plugin/dynamic/native.rs | 506 ++++++++++++++++-- crates/core/src/stream.rs | 7 +- .../tests/fixtures/native_plugin/src/lib.rs | 113 +++- .../tests/integration/api_surface_tests.rs | 8 +- .../core/tests/integration/pipeline_tests.rs | 25 +- .../tests/integration/scope_local_tests.rs | 43 +- crates/core/tests/unit/context_tests.rs | 19 +- crates/core/tests/unit/native_plugin_tests.rs | 92 ++++ crates/ffi/tests/support/mod.rs | 5 + crates/node/plugin.d.ts | 34 +- crates/node/src/api/mod.rs | 213 ++------ crates/node/src/promise_call.rs | 58 +- crates/node/tests/adaptive_tests.mjs | 60 +++ crates/node/tests/llm_tests.mjs | 82 ++- crates/node/tests/scope_local_tests.mjs | 48 ++ crates/node/tests/tools_tests.mjs | 42 +- crates/plugin/src/lib.rs | 101 +++- crates/plugin/tests/typed_callbacks.rs | 24 +- crates/python/src/py_api/mod.rs | 213 +++----- crates/python/src/py_callable.rs | 37 +- crates/python/src/test_support.rs | 13 + .../tests/coverage/py_api_coverage_tests.rs | 12 + .../advanced-guide.mdx | 11 +- docs/reference/migration-guides.mdx | 6 + python/nemo_relay/__init__.py | 4 +- python/nemo_relay/plugin.py | 8 +- python/nemo_relay/subscribers.py | 10 +- python/tests/conftest.py | 7 + .../test_langgraph_integration.py | 2 +- python/tests/test_builtin_codecs.py | 4 +- python/tests/test_event_sanitizers.py | 33 +- python/tests/test_llm.py | 31 +- python/tests/test_scope_local.py | 24 +- python/tests/test_tools.py | 38 +- 37 files changed, 1445 insertions(+), 539 deletions(-) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index fa6e22278..fc77a9bcc 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -171,9 +171,6 @@ mod native { subscribers: &[EventSubscriberFn], scope_stack: ScopeStackHandle, ) -> bool { - if subscribers.is_empty() { - return true; - } let message = DispatcherMessage::Deliver { event: Box::new(event), transform: Some(transform), diff --git a/crates/core/src/api/scope.rs b/crates/core/src/api/scope.rs index 98afdfbb7..3362c303a 100644 --- a/crates/core/src/api/scope.rs +++ b/crates/core/src/api/scope.rs @@ -257,14 +257,13 @@ pub fn push_scope(params: PushScopeParams<'_>) -> Result { (handle, event, subscribers, scope_stack.clone()) }; task_scope_push(handle.clone()); - if let Some(sanitizers) = snapshot_event_sanitizers(&event, &emission_scope_stack) { - let _ = subscriber_dispatcher::dispatch_sanitized_event( - event, - sanitizers, - &subscribers, - emission_scope_stack, - ); - } + let sanitizers = snapshot_event_sanitizers(&event, &emission_scope_stack).unwrap_or_default(); + let _ = subscriber_dispatcher::dispatch_sanitized_event( + event, + sanitizers, + &subscribers, + emission_scope_stack, + ); Ok(handle) } @@ -331,17 +330,15 @@ pub fn pop_scope(params: PopScopeParams<'_>) -> Result<()> { // Capture the scope-local chain before removing its owner. The event is // published later, but scope cleanup must not change the middleware that // was visible when the end event was emitted. - let sanitizers = snapshot_event_sanitizers(&event, &emission_scope_stack); + let sanitizers = snapshot_event_sanitizers(&event, &emission_scope_stack).unwrap_or_default(); let removed = task_scope_remove(params.handle_uuid)?; debug_assert_eq!(removed.uuid, scope.uuid); - if let Some(sanitizers) = sanitizers { - let _ = subscriber_dispatcher::dispatch_sanitized_event( - event, - sanitizers, - &subscribers, - emission_scope_stack, - ); - } + let _ = subscriber_dispatcher::dispatch_sanitized_event( + event, + sanitizers, + &subscribers, + emission_scope_stack, + ); Ok(()) } @@ -406,13 +403,12 @@ pub fn event(params: EmitMarkEventParams<'_>) -> Result<()> { )); (event, subscribers, scope_stack.clone()) }; - if let Some(sanitizers) = snapshot_event_sanitizers(&event, &emission_scope_stack) { - let _ = subscriber_dispatcher::dispatch_sanitized_event( - event, - sanitizers, - &subscribers, - emission_scope_stack, - ); - } + let sanitizers = snapshot_event_sanitizers(&event, &emission_scope_stack).unwrap_or_default(); + let _ = subscriber_dispatcher::dispatch_sanitized_event( + event, + sanitizers, + &subscribers, + emission_scope_stack, + ); Ok(()) } diff --git a/crates/core/src/api/shared.rs b/crates/core/src/api/shared.rs index 5491b3182..40e22159e 100644 --- a/crates/core/src/api/shared.rs +++ b/crates/core/src/api/shared.rs @@ -54,7 +54,7 @@ pub(crate) async fn sanitize_event_with_scope_stack( event: Event, scope_stack: &ScopeStackHandle, ) -> Option { - let entries = snapshot_event_sanitizers(&event, scope_stack)?; + let entries = snapshot_event_sanitizers(&event, scope_stack).unwrap_or_default(); Some(NemoRelayContextState::event_sanitize_snapshot_chain(event, &entries).await) } diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 0f51a25c4..42ba1684b 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -24,9 +24,11 @@ use nemo_relay_plugin::{ NEMO_RELAY_NATIVE_ABI_VERSION, NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY, NemoRelayNativeAsyncCallbackState, NemoRelayNativeAsyncCompletion, NemoRelayNativeAsyncMiddlewareCb, NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativeAsyncNext, - NemoRelayNativeEventSanitizeCb, NemoRelayNativeEventSubscriberCb, NemoRelayNativeFreeFn, - NemoRelayNativeHostApiV1, NemoRelayNativeHostApiV3, NemoRelayNativeLlmCodecKind, - NemoRelayNativeLlmConditionalCb, NemoRelayNativeLlmExecutionCb, NemoRelayNativeLlmRequestCodec, + NemoRelayNativeAsyncNextStreamCb, NemoRelayNativeAsyncStream, + NemoRelayNativeAsyncStreamMiddlewareCb, NemoRelayNativeEventSanitizeCb, + NemoRelayNativeEventSubscriberCb, NemoRelayNativeFreeFn, NemoRelayNativeHostApiV1, + NemoRelayNativeHostApiV3, NemoRelayNativeLlmCodecKind, NemoRelayNativeLlmConditionalCb, + NemoRelayNativeLlmExecutionCb, NemoRelayNativeLlmRequestCodec, NemoRelayNativeLlmRequestInterceptCb, NemoRelayNativeLlmResponseCodec, NemoRelayNativeLlmSanitizeRequestCb, NemoRelayNativeLlmSanitizeRequestContext, NemoRelayNativeLlmSanitizeResponseCb, NemoRelayNativeLlmSanitizeResponseContext, @@ -52,9 +54,9 @@ use crate::api::runtime::{ ToolInterceptFn, ToolSanitizeFn, }; use crate::api::runtime::{ - ScopeStackHandle, ThreadScopeStackBinding, capture_thread_scope_stack, create_scope_stack, - current_scope_stack, restore_thread_scope_stack, scope_stack_active, set_thread_scope_stack, - with_scope_stack, + ScopeStackHandle, TASK_SCOPE_STACK, ThreadScopeStackBinding, capture_thread_scope_stack, + create_scope_stack, current_scope_stack, restore_thread_scope_stack, scope_stack_active, + set_thread_scope_stack, with_scope_stack, }; use crate::api::scope::{ EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeHandle, ScopeType, @@ -876,6 +878,14 @@ fn build_native_host_api_v3() -> NemoRelayNativeHostApiV3 { async_next_invoke: native_async_next_invoke, async_next_release: native_async_next_release, plugin_context_register_async_middleware: native_plugin_context_register_async_middleware, + async_stream_push_json: native_async_stream_push_json, + async_stream_finish: native_async_stream_finish, + async_stream_reject: native_async_stream_reject, + async_stream_is_cancelled: native_async_stream_is_cancelled, + async_stream_release: native_async_stream_release, + async_next_invoke_stream: native_async_next_invoke_stream, + plugin_context_register_async_stream_middleware: + native_plugin_context_register_async_stream_middleware, } } @@ -1317,6 +1327,37 @@ struct NativeCallbackUserData { _instance: Arc, } +struct NativeCallbackUserDataGuard { + ptr: *mut c_void, + free_fn: NemoRelayNativeFreeFn, + armed: bool, +} + +impl NativeCallbackUserDataGuard { + fn new(ptr: *mut c_void, free_fn: NemoRelayNativeFreeFn) -> Self { + Self { + ptr, + free_fn, + armed: true, + } + } + + fn transfer(mut self) -> (*mut c_void, NemoRelayNativeFreeFn) { + self.armed = false; + (self.ptr, self.free_fn) + } +} + +impl Drop for NativeCallbackUserDataGuard { + fn drop(&mut self) { + if self.armed + && let Some(free_fn) = self.free_fn + { + unsafe { free_fn(self.ptr) }; + } + } +} + unsafe impl Send for NativeCallbackUserData {} unsafe impl Sync for NativeCallbackUserData {} @@ -1401,11 +1442,79 @@ enum NativeAsyncNextInner { struct NativeAsyncNext { inner: NativeAsyncNextInner, runtime: tokio::runtime::Handle, + scope_stack: ScopeStackHandle, // The native callback owns this handle independently of its completion. // Retaining the library here prevents an unload while it still uses `next`. _callback_user_data: Option>, } +struct NativeAsyncStream { + sender: Mutex>>>, + cancelled: AtomicBool, + downstream_abort: Mutex>, + _callback_user_data: Option>, +} + +struct NativeAsyncStreamReceiver { + receiver: tokio::sync::mpsc::UnboundedReceiver>, + stream: Arc, +} + +struct NativeAsyncStreamCallbackGuard { + cb: NemoRelayNativeAsyncNextStreamCb, + user_data: usize, + active: bool, +} + +impl NativeAsyncStreamCallbackGuard { + fn finish(&mut self) { + self.active = false; + } +} + +impl Drop for NativeAsyncStreamCallbackGuard { + fn drop(&mut self) { + if self.active { + unsafe { + let _ = (self.cb)( + self.user_data as *mut c_void, + ptr::null(), + ptr::null(), + true, + ); + } + } + } +} + +impl Stream for NativeAsyncStreamReceiver { + type Item = FlowResult; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.receiver.poll_recv(cx) + } +} + +impl Drop for NativeAsyncStreamReceiver { + fn drop(&mut self) { + self.stream.cancelled.store(true, Ordering::Release); + self.stream + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take(); + if let Some(abort) = self + .stream + .downstream_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + abort.abort(); + } + } +} + async fn invoke_native_async_callback( cb: NemoRelayNativeAsyncMiddlewareCb, user_data: Arc, @@ -1435,6 +1544,7 @@ async fn invoke_native_async_callback( (Some(inner), Some(runtime)) => Some(Arc::into_raw(Arc::new(NativeAsyncNext { inner, runtime, + scope_stack: current_scope_stack(), _callback_user_data: Some(user_data.clone()), })) as usize), (None, None) => None, @@ -1456,9 +1566,6 @@ async fn invoke_native_async_callback( drop(Arc::from_raw( completion_ref as *const NativeAsyncCompletion, )); - if let Some(next_ref) = next_ref { - drop(Arc::from_raw(next_ref as *const NativeAsyncNext)); - } native_string_free(invocation as *mut NemoRelayNativeString); } return Err(FlowError::Internal("native async callback panicked".into())); @@ -1472,9 +1579,6 @@ async fn invoke_native_async_callback( drop(Arc::from_raw( completion_ref as *const NativeAsyncCompletion, )); - if let Some(next_ref) = next_ref { - drop(Arc::from_raw(next_ref as *const NativeAsyncNext)); - } } return Err(FlowError::Internal( "native async callback returned an invalid state".into(), @@ -1486,9 +1590,6 @@ async fn invoke_native_async_callback( drop(Arc::from_raw( completion_ref as *const NativeAsyncCompletion, )); - if let Some(next_ref) = next_ref { - drop(Arc::from_raw(next_ref as *const NativeAsyncNext)); - } } if completion .sender @@ -1592,6 +1693,86 @@ unsafe extern "C" fn native_async_next_release(next: *const NemoRelayNativeAsync } } +unsafe extern "C" fn native_async_stream_push_json( + stream: *const NemoRelayNativeAsyncStream, + chunk_json: *const NemoRelayNativeString, +) -> NemoRelayStatus { + let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else { + return NemoRelayStatus::NullPointer; + }; + if stream.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } + let chunk = match parse_json_arg(chunk_json, "native async stream chunk") { + Ok(chunk) => chunk, + Err(status) => return status, + }; + let sender = stream + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clone(); + match sender { + Some(sender) if sender.send(Ok(chunk)).is_ok() => NemoRelayStatus::Ok, + _ => NemoRelayStatus::InvalidArg, + } +} + +unsafe extern "C" fn native_async_stream_finish( + stream: *const NemoRelayNativeAsyncStream, +) -> NemoRelayStatus { + let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else { + return NemoRelayStatus::NullPointer; + }; + if stream + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + .is_some() + { + NemoRelayStatus::Ok + } else { + NemoRelayStatus::InvalidArg + } +} + +unsafe extern "C" fn native_async_stream_reject( + stream: *const NemoRelayNativeAsyncStream, + message: *const NemoRelayNativeString, +) -> NemoRelayStatus { + let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else { + return NemoRelayStatus::NullPointer; + }; + let message = + read_native_string(message).unwrap_or_else(|_| "native async stream rejected".to_string()); + let sender = stream + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take(); + match sender { + Some(sender) => { + let _ = sender.send(Err(FlowError::Internal(message))); + NemoRelayStatus::Ok + } + None => NemoRelayStatus::InvalidArg, + } +} + +unsafe extern "C" fn native_async_stream_is_cancelled( + stream: *const NemoRelayNativeAsyncStream, +) -> bool { + unsafe { (stream as *const NativeAsyncStream).as_ref() } + .is_none_or(|stream| stream.cancelled.load(Ordering::Acquire)) +} + +unsafe extern "C" fn native_async_stream_release(stream: *const NemoRelayNativeAsyncStream) { + if !stream.is_null() { + unsafe { drop(Arc::from_raw(stream as *const NativeAsyncStream)) }; + } +} + /// Invokes the runtime continuation without blocking the calling native thread. unsafe extern "C" fn native_async_next_invoke( next: *const NemoRelayNativeAsyncNext, @@ -1658,17 +1839,128 @@ unsafe extern "C" fn native_async_next_invoke( Box::pin(async move { next(request).await.map(NativeAsyncResult::LlmStream) }) } }; - next.runtime.spawn(async move { - let result = future.await; - if let Some(sender) = completion - .sender - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - { - let _ = sender.send(result); - } - }); + let scope_stack = next.scope_stack.clone(); + next.runtime + .spawn(TASK_SCOPE_STACK.scope(scope_stack, async move { + let result = future.await; + if let Some(sender) = completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + let _ = sender.send(result); + } + })); + NemoRelayStatus::Ok +} + +unsafe extern "C" fn native_async_next_invoke_stream( + next: *const NemoRelayNativeAsyncNext, + invocation_json: *const NemoRelayNativeString, + output_stream: *const NemoRelayNativeAsyncStream, + cb: NemoRelayNativeAsyncNextStreamCb, + user_data: *mut c_void, +) -> NemoRelayStatus { + let Some(next) = (unsafe { (next as *const NativeAsyncNext).as_ref() }) else { + return NemoRelayStatus::NullPointer; + }; + if output_stream.is_null() { + return NemoRelayStatus::NullPointer; + } + unsafe { Arc::increment_strong_count(output_stream as *const NativeAsyncStream) }; + let output_stream = unsafe { Arc::from_raw(output_stream as *const NativeAsyncStream) }; + let NativeAsyncNextInner::LlmStream(next_fn) = &next.inner else { + return NemoRelayStatus::InvalidArg; + }; + let request = match parse_json_arg(invocation_json, "native async stream next invocation") + .and_then(|value| { + serde_json::from_value(value).map_err(|error| { + set_native_last_error(error.to_string()); + NemoRelayStatus::InvalidJson + }) + }) { + Ok(request) => request, + Err(status) => return status, + }; + let next_fn = next_fn.clone(); + let scope_stack = next.scope_stack.clone(); + let library_guard = next._callback_user_data.clone(); + let user_data = user_data as usize; + let task = next + .runtime + .spawn(TASK_SCOPE_STACK.scope(scope_stack, async move { + let _library_guard = library_guard; + let mut callback_guard = NativeAsyncStreamCallbackGuard { + cb, + user_data, + active: true, + }; + match next_fn(request).await { + Ok(mut stream) => { + while let Some(item) = stream.next().await { + match item { + Ok(chunk) => { + if let Some(chunk) = native_string_from_json(&chunk) { + let keep_going = unsafe { + cb(user_data as *mut c_void, chunk, ptr::null(), false) + }; + unsafe { + native_string_free(chunk); + } + if !keep_going { + unsafe { + let _ = cb( + user_data as *mut c_void, + ptr::null(), + ptr::null(), + true, + ); + } + callback_guard.finish(); + return; + } + } else { + break; + } + } + Err(error) => { + if let Some(message) = native_string_from_str(&error.to_string()) { + unsafe { + let _ = cb( + user_data as *mut c_void, + ptr::null(), + message, + false, + ); + native_string_free(message); + } + callback_guard.finish(); + } + return; + } + } + } + unsafe { + let _ = cb(user_data as *mut c_void, ptr::null(), ptr::null(), true); + } + callback_guard.finish(); + } + Err(error) => { + if let Some(message) = native_string_from_str(&error.to_string()) { + unsafe { + let _ = cb(user_data as *mut c_void, ptr::null(), message, false); + native_string_free(message); + } + callback_guard.finish(); + } + } + } + })); + *output_stream + .downstream_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) = Some(task.abort_handle()); NemoRelayStatus::Ok } @@ -1762,12 +2054,12 @@ fn wrap_native_async_llm_sanitize_request( let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |request, context| { let user_data = user_data.clone(); - let codec = format!("{:?}", context.codec()); + let codec = native_async_codec_identity(context.codec()); Box::pin(async move { let value = invoke_native_async_callback( cb, user_data, - serde_json::json!({"request": request, "context": {"codec": codec}}), + serde_json::json!({"request": request, "context": codec}), None, ) .await? @@ -1792,12 +2084,12 @@ fn wrap_native_async_llm_sanitize_response( let user_data = make_user_data(instance, user_data, free_fn); Arc::new(move |response, context| { let user_data = user_data.clone(); - let codec = format!("{:?}", context.codec()); + let codec = native_async_codec_identity(context.codec()); Box::pin(async move { let value = invoke_native_async_callback( cb, user_data, - serde_json::json!({"response": response, "context": {"codec": codec}}), + serde_json::json!({"response": response, "context": codec}), None, ) .await? @@ -1807,6 +2099,23 @@ fn wrap_native_async_llm_sanitize_response( }) } +fn native_async_codec_identity(identity: &LlmCodecIdentity) -> Json { + match identity { + LlmCodecIdentity::None => { + serde_json::json!({"codec_kind": "none", "codec_id": Json::Null}) + } + LlmCodecIdentity::BuiltIn(codec) => { + serde_json::json!({"codec_kind": "builtin", "codec_id": codec.id()}) + } + LlmCodecIdentity::Runtime(id) => { + serde_json::json!({"codec_kind": "runtime", "codec_id": id}) + } + LlmCodecIdentity::Opaque => { + serde_json::json!({"codec_kind": "opaque", "codec_id": Json::Null}) + } + } +} + fn wrap_native_async_llm_request_intercept( instance: Arc, cb: NemoRelayNativeAsyncMiddlewareCb, @@ -1949,6 +2258,118 @@ fn wrap_native_async_llm_stream_execution( }) } +fn wrap_native_incremental_llm_stream_execution( + instance: Arc, + cb: NemoRelayNativeAsyncStreamMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> LlmStreamExecutionFn { + let user_data = make_user_data(instance, user_data, free_fn); + Arc::new(move |name, request, next| { + let user_data = user_data.clone(); + let name = name.to_owned(); + Box::pin(async move { + let invocation = + native_string_from_json(&serde_json::json!({"name": name, "request": request})) + .ok_or_else(|| { + FlowError::Internal( + "failed to allocate native async stream invocation".into(), + ) + })?; + let runtime = tokio::runtime::Handle::try_current().map_err(|error| { + FlowError::Internal(format!( + "native async stream intercept requires a Tokio runtime: {error}" + )) + })?; + let next_ref = Arc::into_raw(Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::LlmStream(next), + runtime, + scope_stack: current_scope_stack(), + _callback_user_data: Some(user_data.clone()), + })); + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + _callback_user_data: Some(user_data.clone()), + }); + let stream_ref = Arc::into_raw(stream.clone()); + let state = catch_unwind(AssertUnwindSafe(|| unsafe { + cb( + user_data.ptr, + invocation, + next_ref as *const NemoRelayNativeAsyncNext, + stream_ref as *const NemoRelayNativeAsyncStream, + ) + })); + unsafe { native_string_free(invocation) }; + let state = match state + .ok() + .and_then(|state| NemoRelayNativeAsyncCallbackState::try_from(state).ok()) + { + Some(state) => state, + None => { + stream + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take(); + return Err(FlowError::Internal( + "native async stream callback panicked or returned an invalid state".into(), + )); + } + }; + if state == NemoRelayNativeAsyncCallbackState::Complete + && stream + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_some() + { + return Err(FlowError::Internal( + "native async stream callback returned Complete without finishing".into(), + )); + } + Ok(LlmJsonStream::new(NativeAsyncStreamReceiver { + receiver, + stream, + })) + }) + }) +} + +unsafe extern "C" fn native_plugin_context_register_async_stream_middleware( + ctx: *mut NemoRelayNativePluginContext, + name: *const NemoRelayNativeString, + priority: i32, + cb: NemoRelayNativeAsyncStreamMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, +) -> NemoRelayStatus { + clear_native_last_error(); + let user_data_guard = NativeCallbackUserDataGuard::new(user_data, free_fn); + let host_ctx = match host_ctx_mut(ctx) { + Ok(ctx) => ctx, + Err(status) => return status, + }; + let instance = host_ctx.instance.clone(); + let name = match read_name(name) { + Ok(name) => name, + Err(status) => return status, + }; + let (user_data, free_fn) = user_data_guard.transfer(); + let context = unsafe { &mut *host_ctx.ctx }; + match context.register_llm_stream_execution_intercept( + &name, + priority, + wrap_native_incremental_llm_stream_execution(instance, cb, user_data, free_fn), + ) { + Ok(()) => NemoRelayStatus::Ok, + Err(error) => status_from_plugin_error(error), + } +} + unsafe extern "C" fn native_plugin_context_register_async_middleware( ctx: *mut NemoRelayNativePluginContext, kind: u32, @@ -1960,6 +2381,9 @@ unsafe extern "C" fn native_plugin_context_register_async_middleware( free_fn: NemoRelayNativeFreeFn, ) -> NemoRelayStatus { clear_native_last_error(); + // The host owns callback user data as soon as registration is attempted, + // including malformed and incompatible registrations. + let user_data_guard = NativeCallbackUserDataGuard::new(user_data, free_fn); let host_ctx = match host_ctx_mut(ctx) { Ok(ctx) => ctx, Err(status) => return status, @@ -1976,6 +2400,15 @@ unsafe extern "C" fn native_plugin_context_register_async_middleware( return NemoRelayStatus::InvalidArg; } }; + if kind == NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept + && let Err(error) = validate_annotated_request_consumer_compatibility( + &instance.relay_compat, + &instance.plugin_kind, + ) + { + return status_from_plugin_error(error); + } + let (user_data, free_fn) = user_data_guard.transfer(); let context = unsafe { &mut *host_ctx.ctx }; let registration = match kind { NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest => context @@ -2027,20 +2460,13 @@ unsafe extern "C" fn native_plugin_context_register_async_middleware( priority, wrap_native_async_llm_conditional(instance, cb, user_data, free_fn), ), - NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept => { - if let Err(error) = validate_annotated_request_consumer_compatibility( - &instance.relay_compat, - &instance.plugin_kind, - ) { - return status_from_plugin_error(error); - } - context.register_llm_request_intercept( + NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept => context + .register_llm_request_intercept( &name, priority, break_chain, wrap_native_async_llm_request_intercept(instance, cb, user_data, free_fn), - ) - } + ), NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept => context .register_llm_execution_intercept( &name, diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index a1c02179b..d28899d01 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -188,11 +188,12 @@ impl LlmStreamWrapper { "ERROR", Some("stream dropped before clean completion".to_string()), ); - // Drop cannot await the async finalizer. Close the recorder before - // spawning it so late optimization evidence is rejected immediately. + // Drop cannot await the async finalizer. Seal contribution acceptance + // immediately, but let the finalizer decide whether authoritative + // terminal usage means the stream should be marked interrupted. self.handle .optimization_recorder - .close_for_finalization(Some("stream_interrupted")); + .close_for_finalization(None); self.finalization = self.emit_end_event(metadata, true, background_thread); } diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index 46c05c265..753948ee4 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -9,10 +9,11 @@ use nemo_relay_plugin::{ CategoryProfile, ConfigDiagnostic, DiagnosticLevel, Event, EventCategory, EventSanitizeFields, Json, LlmJsonStream, LlmRequest, LlmRequestInterceptOutcome, NativePlugin, NemoRelayNativeAsyncCallbackState, NemoRelayNativeAsyncMiddlewareCb, - NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativeHostApiV1, NemoRelayNativeHostApiV3, - NemoRelayNativePluginContext, NemoRelayNativePluginV1, NemoRelayNativeString, - NemoRelayNativeToolNextFn, NemoRelayStatus, PendingMarkSpec, PluginContext, PluginRuntime, - ScopeCategory, ScopeType, ToolExecutionInterceptOutcome, + NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativeAsyncNext, NemoRelayNativeAsyncStream, + NemoRelayNativeHostApiV1, NemoRelayNativeHostApiV3, NemoRelayNativePluginContext, + NemoRelayNativePluginV1, NemoRelayNativeString, NemoRelayNativeToolNextFn, NemoRelayStatus, + PendingMarkSpec, PluginContext, PluginRuntime, ScopeCategory, ScopeType, + ToolExecutionInterceptOutcome, }; use serde_json::{Map, json}; @@ -627,7 +628,7 @@ unsafe extern "C" fn raw_register_async_tool_request( NemoRelayNativeAsyncMiddlewareKind, &str, NemoRelayNativeAsyncMiddlewareCb, - ); 14] = [ + ); 13] = [ ( NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest, "fixture_async_tool_sanitize_request", @@ -678,11 +679,6 @@ unsafe extern "C" fn raw_register_async_tool_request( "fixture_async_llm_execution", raw_async_tool_execution_callback, ), - ( - NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept, - "fixture_async_llm_stream", - raw_async_tool_execution_callback, - ), ( NemoRelayNativeAsyncMiddlewareKind::MarkSanitize, "fixture_async_mark", @@ -721,9 +717,106 @@ unsafe extern "C" fn raw_register_async_tool_request( return status; } } + let name = unsafe { raw_host_string(&host.v1, "fixture_async_llm_stream") }; + if name.is_null() { + return NemoRelayStatus::Internal; + } + let status = unsafe { + (host.plugin_context_register_async_stream_middleware)( + ctx, + name, + 0, + raw_async_stream_callback, + user_data, + None, + ) + }; + unsafe { (host.v1.string_free)(name) }; + if status != NemoRelayStatus::Ok { + return status; + } NemoRelayStatus::Ok } +struct AsyncStreamForward { + host: NemoRelayNativeHostApiV3, + stream: *const NemoRelayNativeAsyncStream, +} + +unsafe extern "C" fn raw_async_stream_forward( + user_data: *mut c_void, + chunk: *const NemoRelayNativeString, + error: *const NemoRelayNativeString, + done: bool, +) -> bool { + let state = unsafe { &*(user_data as *const AsyncStreamForward) }; + if !chunk.is_null() { + return unsafe { (state.host.async_stream_push_json)(state.stream, chunk) } + == NemoRelayStatus::Ok; + } + if !error.is_null() { + unsafe { (state.host.async_stream_reject)(state.stream, error) }; + } else if done { + unsafe { (state.host.async_stream_finish)(state.stream) }; + } else { + return true; + } + unsafe { + (state.host.async_stream_release)(state.stream); + drop(Box::from_raw(user_data as *mut AsyncStreamForward)); + } + false +} + +unsafe extern "C" fn raw_async_stream_callback( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + next: *const NemoRelayNativeAsyncNext, + stream: *const NemoRelayNativeAsyncStream, +) -> u32 { + let Some(host) = (unsafe { (user_data as *const NemoRelayNativeHostApiV3).as_ref() }) else { + return NemoRelayNativeAsyncCallbackState::Complete as u32; + }; + let request = unsafe { raw_host_string_value(&host.v1, invocation_json) } + .and_then(|json| serde_json::from_str::(&json).ok()) + .and_then(|invocation| invocation.get("request").cloned()) + .and_then(|request| serde_json::to_string(&request).ok()) + .map(|request| unsafe { raw_host_string(&host.v1, &request) }); + let Some(request) = request.filter(|request| !request.is_null()) else { + unsafe { + (host.async_next_release)(next); + (host.async_stream_release)(stream); + } + return NemoRelayNativeAsyncCallbackState::Complete as u32; + }; + let state = Box::into_raw(Box::new(AsyncStreamForward { + host: *host, + stream, + })); + let status = unsafe { + (host.async_next_invoke_stream)( + next, + request, + stream, + raw_async_stream_forward, + state.cast(), + ) + }; + unsafe { + (host.v1.string_free)(request); + (host.async_next_release)(next); + } + if status == NemoRelayStatus::Ok { + NemoRelayNativeAsyncCallbackState::Pending as u32 + } else { + unsafe { + drop(Box::from_raw(state)); + (host.async_stream_release)(stream); + } + NemoRelayNativeAsyncCallbackState::Complete as u32 + } +} + unsafe extern "C" fn raw_async_allow_callback( user_data: *mut c_void, _invocation_json: *const NemoRelayNativeString, diff --git a/crates/core/tests/integration/api_surface_tests.rs b/crates/core/tests/integration/api_surface_tests.rs index 5b14fe82f..7b0f72ffc 100644 --- a/crates/core/tests/integration/api_surface_tests.rs +++ b/crates/core/tests/integration/api_surface_tests.rs @@ -739,7 +739,13 @@ fn test_manual_llm_end_queues_optimization_marks_before_end_event() { ) .unwrap(); - let names = captured_events_snapshot(&events) + let captured = captured_events_snapshot(&events); + let optimization = captured + .iter() + .find(|event| event.name() == "nemo_relay.llm.optimization") + .expect("manual LLM optimization mark"); + assert_eq!(optimization.parent_uuid(), Some(handle.uuid)); + let names = captured .into_iter() .filter(|event| { event.name() == "manual-optimized-llm" || event.name() == "nemo_relay.llm.optimization" diff --git a/crates/core/tests/integration/pipeline_tests.rs b/crates/core/tests/integration/pipeline_tests.rs index 999369412..9cb7e67d6 100644 --- a/crates/core/tests/integration/pipeline_tests.rs +++ b/crates/core/tests/integration/pipeline_tests.rs @@ -2086,23 +2086,22 @@ async fn test_dropped_stream_end_keeps_fifo_position_before_later_mark() { ) .unwrap(); - let (flush_done_tx, flush_done_rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let result = flush_subscribers(); - let _ = flush_done_tx.send(result); + let (release_started_tx, release_started_rx) = std::sync::mpsc::channel(); + let release_thread = std::thread::spawn(move || { + release_started_tx.send(()).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(50)); + sanitizer_release.notify_one(); }); - let flush_waited_for_end = flush_done_rx - .recv_timeout(std::time::Duration::from_millis(50)) - .is_err(); - sanitizer_release.notify_one(); + release_started_rx + .recv_timeout(std::time::Duration::from_secs(2)) + .expect("sanitizer release thread did not start"); + let flush_started = std::time::Instant::now(); + flush_subscribers().unwrap(); assert!( - flush_waited_for_end, + flush_started.elapsed() >= std::time::Duration::from_millis(50), "flush must wait for the pending stream END" ); - flush_done_rx - .recv_timeout(std::time::Duration::from_secs(2)) - .expect("flush did not finish after sanitizer release") - .unwrap(); + release_thread.join().unwrap(); let events = events.lock().unwrap(); let end_index = events diff --git a/crates/core/tests/integration/scope_local_tests.rs b/crates/core/tests/integration/scope_local_tests.rs index ceb17e821..a2ec4616f 100644 --- a/crates/core/tests/integration/scope_local_tests.rs +++ b/crates/core/tests/integration/scope_local_tests.rs @@ -9,6 +9,8 @@ #![allow(clippy::await_holding_lock)] +mod test_support; + use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; @@ -31,6 +33,7 @@ use nemo_relay::api::subscriber::{ use nemo_relay::api::tool::{tool_call, tool_call_end, tool_call_execute}; use nemo_relay::error::FlowError; use serde_json::json; +use test_support::ready; // All tests share the global context, so we serialize them. static TEST_MUTEX: Mutex<()> = Mutex::new(()); @@ -84,7 +87,7 @@ fn test_scope_local_guardrail_registration_and_execution() { args.as_object_mut() .unwrap() .insert("scope_sanitized".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -166,13 +169,13 @@ async fn test_auto_cleanup_on_scope_pop() { args.as_object_mut() .unwrap() .insert("ephemeral".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); // Verify it runs before pop. - let func: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func: ToolExecutionNextFn = Arc::new(|args| ready(args)); let result = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("tool") @@ -193,7 +196,7 @@ async fn test_auto_cleanup_on_scope_pop() { .unwrap(); // Now execute again — the field should NOT appear. - let func2: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func2: ToolExecutionNextFn = Arc::new(|args| ready(args)); let result2 = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("tool") @@ -234,7 +237,7 @@ async fn test_priority_merge_global_and_scope_local() { args.as_object_mut() .unwrap() .insert("p10".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -250,7 +253,7 @@ async fn test_priority_merge_global_and_scope_local() { args.as_object_mut() .unwrap() .insert("p30".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -267,12 +270,12 @@ async fn test_priority_merge_global_and_scope_local() { args.as_object_mut() .unwrap() .insert("p20".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); - let func: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func: ToolExecutionNextFn = Arc::new(|args| ready(args)); let result = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("tool") @@ -326,7 +329,7 @@ fn test_name_coexistence_global_and_scope_local() { 1, Arc::new(move |_name, args| { c1.fetch_add(1, Ordering::SeqCst); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -339,7 +342,7 @@ fn test_name_coexistence_global_and_scope_local() { 2, Arc::new(move |_name, args| { c2.fetch_add(1, Ordering::SeqCst); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -401,7 +404,7 @@ async fn test_scope_isolation_between_stacks() { args.as_object_mut() .unwrap() .insert("agent".into(), json!("a")); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -427,7 +430,7 @@ async fn test_scope_isolation_between_stacks() { args.as_object_mut() .unwrap() .insert("agent".into(), json!("b")); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -436,7 +439,7 @@ async fn test_scope_isolation_between_stacks() { // Execute on stack A — should see agent_a's intercept only set_thread_scope_stack(stack_a.clone()); - let func_a: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func_a: ToolExecutionNextFn = Arc::new(|args| ready(args)); let result_a = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("tool") @@ -450,7 +453,7 @@ async fn test_scope_isolation_between_stacks() { // Execute on stack B — should see agent_b's intercept only set_thread_scope_stack(stack_b.clone()); - let func_b: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func_b: ToolExecutionNextFn = Arc::new(|args| ready(args)); let result_b = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("tool") @@ -507,7 +510,7 @@ async fn test_nested_scope_inheritance() { args.as_object_mut() .unwrap() .insert("global".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -531,7 +534,7 @@ async fn test_nested_scope_inheritance() { args.as_object_mut() .unwrap() .insert("scope_a".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); @@ -556,13 +559,13 @@ async fn test_nested_scope_inheritance() { args.as_object_mut() .unwrap() .insert("scope_b".into(), json!(true)); - Box::pin(async move { Ok(args) }) + ready(args) }), ) .unwrap(); // Execute within scope B — should see global + scope_a + scope_b - let func: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func: ToolExecutionNextFn = Arc::new(|args| ready(args)); let result = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("tool") @@ -715,7 +718,7 @@ async fn test_scope_local_conditional_execution_guardrail() { .unwrap(); // Call to banned_tool should be rejected - let func_banned: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func_banned: ToolExecutionNextFn = Arc::new(|args| ready(args)); let err = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("banned_tool") @@ -734,7 +737,7 @@ async fn test_scope_local_conditional_execution_guardrail() { } // Call to a different tool should succeed - let func_ok: ToolExecutionNextFn = Arc::new(|args| Box::pin(async move { Ok(args) })); + let func_ok: ToolExecutionNextFn = Arc::new(|args| ready(args)); let result = tool_call_execute( nemo_relay::api::tool::ToolCallExecuteParams::builder() .name("allowed_tool") diff --git a/crates/core/tests/unit/context_tests.rs b/crates/core/tests/unit/context_tests.rs index 48853939d..8481d15c7 100644 --- a/crates/core/tests/unit/context_tests.rs +++ b/crates/core/tests/unit/context_tests.rs @@ -271,8 +271,8 @@ async fn conditional_guardrail_snapshots_keep_names_and_callbacks_after_deregist ); } -#[test] -fn context_state_supports_extensions_events_and_builders() { +#[tokio::test] +async fn context_state_supports_extensions_events_and_builders() { let mut state = NemoRelayContextState::new(); assert!(state.extensions.is_empty()); @@ -323,14 +323,13 @@ fn context_state_supports_extensions_events_and_builders() { content: json!({"messages": []}), }; let entries = state.llm_sanitize_request_entries(&[]); - let sanitized = tokio::runtime::Runtime::new() - .unwrap() - .block_on(NemoRelayContextState::llm_sanitize_request_snapshot_chain( - request.clone(), - crate::api::runtime::LlmSanitizeRequestContext::default(), - &entries, - )) - .expect("an empty sanitizer chain must retain the request"); + let sanitized = NemoRelayContextState::llm_sanitize_request_snapshot_chain( + request.clone(), + crate::api::runtime::LlmSanitizeRequestContext::default(), + &entries, + ) + .await + .expect("an empty sanitizer chain must retain the request"); assert!(sanitized.headers.is_empty()); let events = Arc::new(Mutex::new(Vec::::new())); diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index 934e0c3ee..4c3aa9848 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -290,6 +290,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { let next = Arc::new(NativeAsyncNext { inner, runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), _callback_user_data: None, }); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; @@ -324,6 +325,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { }) })), runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), _callback_user_data: None, }); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; @@ -417,6 +419,50 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen unsafe { native_async_completion_release(completion_ref) }; } +#[test] +fn native_async_stream_push_finish_and_consumer_cancellation_are_incremental() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; + let chunk = native_string(r#"{"chunk":1}"#); + assert_eq!( + unsafe { native_async_stream_push_json(stream_ref, chunk) }, + NemoRelayStatus::Ok + ); + let mut receiver = NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }; + assert_eq!( + runtime.block_on(receiver.next()).unwrap().unwrap(), + json!({"chunk": 1}) + ); + assert_eq!( + unsafe { native_async_stream_finish(stream_ref) }, + NemoRelayStatus::Ok + ); + assert!(runtime.block_on(receiver.next()).is_none()); + drop(receiver); + assert!(unsafe { native_async_stream_is_cancelled(stream_ref) }); + assert_eq!( + unsafe { native_async_stream_push_json(stream_ref, chunk) }, + NemoRelayStatus::InvalidArg + ); + unsafe { + native_string_free(chunk); + native_async_stream_release(stream_ref); + } +} + #[test] fn native_timestamp_scope_type_and_error_mappings_cover_variants() { assert_eq!(optional_timestamp_from_native(ptr::null()).unwrap(), None); @@ -1385,6 +1431,52 @@ fn native_llm_sanitize_context_preserves_all_codec_identity_states() { } } +#[test] +fn native_async_llm_sanitize_context_uses_stable_codec_envelope() { + assert_eq!( + native_async_codec_identity(&LlmCodecIdentity::None), + json!({"codec_kind": "none", "codec_id": null}) + ); + assert_eq!( + native_async_codec_identity(&LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat)), + json!({"codec_kind": "builtin", "codec_id": "openai_chat"}) + ); + assert_eq!( + native_async_codec_identity(&LlmCodecIdentity::Runtime("com.example.chat.v1".into())), + json!({"codec_kind": "runtime", "codec_id": "com.example.chat.v1"}) + ); + assert_eq!( + native_async_codec_identity(&LlmCodecIdentity::Opaque), + json!({"codec_kind": "opaque", "codec_id": null}) + ); +} + +unsafe extern "C" fn count_user_data_free(user_data: *mut c_void) { + let count = unsafe { &*(user_data as *const AtomicUsize) }; + count.fetch_add(1, Ordering::SeqCst); +} + +#[test] +fn native_async_registration_user_data_guard_frees_or_transfers_exactly_once() { + let frees = AtomicUsize::new(0); + { + let _guard = NativeCallbackUserDataGuard::new( + (&frees as *const AtomicUsize).cast_mut().cast(), + Some(count_user_data_free), + ); + } + assert_eq!(frees.load(Ordering::SeqCst), 1); + + let transferred = NativeCallbackUserDataGuard::new( + (&frees as *const AtomicUsize).cast_mut().cast(), + Some(count_user_data_free), + ) + .transfer(); + assert_eq!(frees.load(Ordering::SeqCst), 1); + unsafe { transferred.1.unwrap()(transferred.0) }; + assert_eq!(frees.load(Ordering::SeqCst), 2); +} + #[test] fn native_llm_sanitizer_input_allocation_failures_release_codec_ids() { let request = LlmRequest { diff --git a/crates/ffi/tests/support/mod.rs b/crates/ffi/tests/support/mod.rs index 8e557b330..59f6e28e5 100644 --- a/crates/ffi/tests/support/mod.rs +++ b/crates/ffi/tests/support/mod.rs @@ -5,6 +5,11 @@ use std::future::Future; +/// Resolve a future on a fresh current-thread runtime. +/// +/// This helper cannot drive wrappers whose futures call +/// [`tokio::task::block_in_place`], such as execution-intercept trampolines; +/// those tests must use a multi-thread runtime. pub(crate) fn resolve(future: impl Future) -> T { tokio::runtime::Builder::new_current_thread() .enable_all() diff --git a/crates/node/plugin.d.ts b/crates/node/plugin.d.ts index cc0a130b3..21f8771dc 100644 --- a/crates/node/plugin.d.ts +++ b/crates/node/plugin.d.ts @@ -195,31 +195,31 @@ export interface PluginContext { registerMarkSanitizeGuardrail( name: string, priority: number, - callback: (event: Json, fields: EventSanitizeFields) => EventSanitizeFields, + callback: (event: Json, fields: EventSanitizeFields) => EventSanitizeFields | Promise, ): void; /** Register a scope-start event sanitizer for this component. */ registerScopeSanitizeStartGuardrail( name: string, priority: number, - callback: (event: Json, fields: EventSanitizeFields) => EventSanitizeFields, + callback: (event: Json, fields: EventSanitizeFields) => EventSanitizeFields | Promise, ): void; /** Register a scope-end event sanitizer for this component. */ registerScopeSanitizeEndGuardrail( name: string, priority: number, - callback: (event: Json, fields: EventSanitizeFields) => EventSanitizeFields, + callback: (event: Json, fields: EventSanitizeFields) => EventSanitizeFields | Promise, ): void; /** Register a tool sanitize-request guardrail for this component. */ registerToolSanitizeRequestGuardrail( name: string, priority: number, - callback: (name: string, args: Json) => Json, + callback: (name: string, args: Json) => Json | Promise, ): void; /** Register a tool sanitize-response guardrail for this component. */ registerToolSanitizeResponseGuardrail( name: string, priority: number, - callback: (name: string, result: Json) => Json, + callback: (name: string, result: Json) => Json | Promise, ): void; /** Register a tool conditional-execution guardrail for this component. */ registerToolConditionalExecutionGuardrail( @@ -231,26 +231,30 @@ export interface PluginContext { registerLlmSanitizeRequestGuardrail( name: string, priority: number, - callback: (request: Json, context: LlmSanitizeRequestContext) => Json | null, + callback: (request: Json, context: LlmSanitizeRequestContext) => Json | null | Promise, ): void; /** Register an LLM sanitize-response guardrail. The callback receives `(response, context)`. */ registerLlmSanitizeResponseGuardrail( name: string, priority: number, - callback: (response: Json, context: LlmSanitizeResponseContext) => Json | null, + callback: (response: Json, context: LlmSanitizeResponseContext) => Json | null | Promise, ): void; /** Register an LLM conditional-execution guardrail for this component. */ registerLlmConditionalExecutionGuardrail( name: string, priority: number, - callback: (request: Json) => string | null, + callback: (request: Json) => string | null | Promise, ): void; /** Register an LLM request intercept for this component. */ registerLlmRequestIntercept( name: string, priority: number, breakChain: boolean, - callback: (args: { name: string; request: Json; annotated: Json | null }) => LlmRequestInterceptOutcome, + callback: (args: { + name: string; + request: Json; + annotated: Json | null; + }) => LlmRequestInterceptOutcome | Promise, ): void; /** Register an LLM execution intercept for this component. */ registerLlmExecutionIntercept( @@ -258,14 +262,16 @@ export interface PluginContext { priority: number, callback: (request: Json, next: (request: Json) => Json | Promise) => Json | Promise, ): void; - /** Register an LLM streaming execution intercept for this component. */ + /** + * Register an LLM streaming execution intercept for this component. + * + * The `next` callback resolves to all downstream chunks. Returning an array + * preserves those chunks; any other JSON value produces one chunk. + */ registerLlmStreamExecutionIntercept( name: string, priority: number, - callback: ( - request: Json, - next: (request: Json) => AsyncIterable | Promise>, - ) => AsyncIterable | Promise>, + callback: (request: Json, next: (request: Json) => Promise) => Json | Json[] | Promise, ): void; /** Register a tool request intercept for this component. */ registerToolRequestIntercept( diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 048429c47..872420717 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -458,69 +458,6 @@ pub fn end_stream(stream_id: f64) { finish_stream_channel(id, Ok(())); } -#[allow(clippy::enum_variant_names)] -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -enum PromiseAwareKey { - GlobalToolExecution(String), - GlobalLlmExecution(String), - GlobalLlmStreamExecution(String), - ScopeToolExecution { scope_uuid: String, name: String }, - ScopeLlmExecution { scope_uuid: String, name: String }, - ScopeLlmStreamExecution { scope_uuid: String, name: String }, -} - -impl PromiseAwareKey { - fn scope_uuid(&self) -> Option<&str> { - match self { - Self::ScopeToolExecution { scope_uuid, .. } - | Self::ScopeLlmExecution { scope_uuid, .. } - | Self::ScopeLlmStreamExecution { scope_uuid, .. } => Some(scope_uuid), - Self::GlobalToolExecution(_) - | Self::GlobalLlmExecution(_) - | Self::GlobalLlmStreamExecution(_) => None, - } - } -} - -static PROMISE_AWARE_REGISTRATIONS: std::sync::LazyLock< - StdMutex>>, -> = std::sync::LazyLock::new(|| StdMutex::new(HashMap::new())); - -fn remember_promise_aware( - key: PromiseAwareKey, - pa_fn: std::sync::Arc, -) { - if let Some(previous) = PROMISE_AWARE_REGISTRATIONS - .lock() - .unwrap() - .insert(key, pa_fn) - { - previous.close(); - } -} - -fn forget_promise_aware(key: &PromiseAwareKey) { - if let Some(pa_fn) = PROMISE_AWARE_REGISTRATIONS.lock().unwrap().remove(key) { - pa_fn.close(); - } -} - -fn forget_scope_local_promise_aware(scope_uuid: &str) { - let mut registrations = PROMISE_AWARE_REGISTRATIONS.lock().unwrap(); - let keys = registrations - .keys() - .filter(|key| key.scope_uuid() == Some(scope_uuid)) - .cloned() - .collect::>(); - - for key in keys { - let registration = registrations.remove(&key); - if let Some(pa_fn) = registration { - pa_fn.close(); - } - } -} - /// # Safety /// Both `env` and `value` must contain valid N-API handles that point to live /// JavaScript objects in the same environment. The caller must also ensure the @@ -1060,13 +997,12 @@ fn build_plugin_context( let name = format!("{}{}", llm_exec_namespace, ctx.get::(0)?); let priority = ctx.get::(1)?; let callback = ctx.get::(2)?; - let promise_fn = Arc::new(crate::promise_call::PromiseAwareFn::new( - ctx.env, &callback, - )?); core_registry_api::register_llm_execution_intercept( &name, priority, - callable::wrap_js_llm_exec_intercept_fn(promise_fn.clone()), + callable::wrap_js_llm_exec_intercept_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -1075,15 +1011,13 @@ fn build_plugin_context( "plugin", name_clone.clone(), Box::new(move || { - let result = core_registry_api::deregister_llm_execution_intercept(&name_clone) + core_registry_api::deregister_llm_execution_intercept(&name_clone) .map(|_| ()) .map_err(|e| { PluginError::RegistrationFailed(format!( "llm execution intercept deregistration failed: {e}" )) - }); - promise_fn.close(); - result + }) }), )); ctx.env.get_undefined() @@ -1102,13 +1036,12 @@ fn build_plugin_context( let name = format!("{}{}", llm_stream_namespace, ctx.get::(0)?); let priority = ctx.get::(1)?; let callback = ctx.get::(2)?; - let promise_fn = Arc::new(crate::promise_call::PromiseAwareFn::new( - ctx.env, &callback, - )?); core_registry_api::register_llm_stream_execution_intercept( &name, priority, - callable::wrap_js_llm_stream_exec_intercept_fn(promise_fn.clone()), + callable::wrap_js_llm_stream_exec_intercept_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -1120,17 +1053,13 @@ fn build_plugin_context( "plugin", name_clone.clone(), Box::new(move || { - let result = core_registry_api::deregister_llm_stream_execution_intercept( - &name_clone, - ) - .map(|_| ()) - .map_err(|e| { - PluginError::RegistrationFailed(format!( - "llm stream execution intercept deregistration failed: {e}" - )) - }); - promise_fn.close(); - result + core_registry_api::deregister_llm_stream_execution_intercept(&name_clone) + .map(|_| ()) + .map_err(|e| { + PluginError::RegistrationFailed(format!( + "llm stream execution intercept deregistration failed: {e}" + )) + }) }), )); ctx.env.get_undefined() @@ -1193,13 +1122,12 @@ fn build_plugin_context( let name = format!("{}{}", tool_exec_namespace, ctx.get::(0)?); let priority = ctx.get::(1)?; let callback = ctx.get::(2)?; - let promise_fn = Arc::new(crate::promise_call::PromiseAwareFn::new( - ctx.env, &callback, - )?); core_registry_api::register_tool_execution_intercept( &name, priority, - callable::wrap_js_tool_exec_intercept_fn(promise_fn.clone()), + callable::wrap_js_tool_exec_intercept_fn(Arc::new(PromiseAwareFn::new( + ctx.env, &callback, + )?)), ) .map_err(to_napi_err)?; @@ -1208,16 +1136,13 @@ fn build_plugin_context( "plugin", name_clone.clone(), Box::new(move || { - let result = - core_registry_api::deregister_tool_execution_intercept(&name_clone) - .map(|_| ()) - .map_err(|e| { - PluginError::RegistrationFailed(format!( - "tool execution intercept deregistration failed: {e}" - )) - }); - promise_fn.close(); - result + core_registry_api::deregister_tool_execution_intercept(&name_clone) + .map(|_| ()) + .map_err(|e| { + PluginError::RegistrationFailed(format!( + "tool execution intercept deregistration failed: {e}" + )) + }) }), )); ctx.env.get_undefined() @@ -1923,7 +1848,6 @@ pub fn pop_scope( .build(), ) .map_err(to_napi_err)?; - forget_scope_local_promise_aware(&handle.inner.uuid.to_string()); Ok(()) } @@ -2015,16 +1939,12 @@ pub fn with_scope( Err(error) => otel_status_metadata("ERROR", Some(error.to_string())), }; // Always pop the scope, even on error. - if core_scope_api::pop_scope( + let _ = core_scope_api::pop_scope( core_scope_api::PopScopeParams::builder() .handle_uuid(&scope_uuid) .metadata_opt(Some(metadata)) .build(), - ) - .is_ok() - { - forget_scope_local_promise_aware(&scope_uuid.to_string()); - } + ); result.map_err(to_napi_err) }) .await @@ -2884,7 +2804,6 @@ pub fn register_tool_execution_intercept( )] callable: JsFunction, ) -> Result<()> { - let key = PromiseAwareKey::GlobalToolExecution(name.clone()); let pa_fn = std::sync::Arc::new( crate::promise_call::PromiseAwareFn::new(&env, &callable).map_err(|e| { napi::Error::from_reason(format!("failed to create PromiseAwareFn: {e}")) @@ -2896,7 +2815,6 @@ pub fn register_tool_execution_intercept( callable::wrap_js_tool_exec_intercept_fn(pa_fn.clone()), ) .map_err(to_napi_err)?; - remember_promise_aware(key, pa_fn); Ok(()) } @@ -2905,13 +2823,7 @@ pub fn register_tool_execution_intercept( /// Returns `true` if an intercept with that name was found and removed. #[napi] pub fn deregister_tool_execution_intercept(name: String) -> Result { - let key = PromiseAwareKey::GlobalToolExecution(name.clone()); - let removed = - core_registry_api::deregister_tool_execution_intercept(&name).map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + core_registry_api::deregister_tool_execution_intercept(&name).map_err(to_napi_err) } // --------------------------------------------------------------------------- @@ -3075,7 +2987,6 @@ pub fn register_llm_execution_intercept( priority: i32, callable: JsFunction, ) -> Result<()> { - let key = PromiseAwareKey::GlobalLlmExecution(name.clone()); let pa_fn = std::sync::Arc::new( crate::promise_call::PromiseAwareFn::new(&env, &callable).map_err(|e| { napi::Error::from_reason(format!("failed to create PromiseAwareFn: {e}")) @@ -3087,7 +2998,6 @@ pub fn register_llm_execution_intercept( callable::wrap_js_llm_exec_intercept_fn(pa_fn.clone()), ) .map_err(to_napi_err)?; - remember_promise_aware(key, pa_fn); Ok(()) } @@ -3096,13 +3006,7 @@ pub fn register_llm_execution_intercept( /// Returns `true` if an intercept with that name was found and removed. #[napi] pub fn deregister_llm_execution_intercept(name: String) -> Result { - let key = PromiseAwareKey::GlobalLlmExecution(name.clone()); - let removed = - core_registry_api::deregister_llm_execution_intercept(&name).map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + core_registry_api::deregister_llm_execution_intercept(&name).map_err(to_napi_err) } /// Register a streaming LLM execution intercept following the middleware chain pattern. @@ -3118,7 +3022,6 @@ pub fn register_llm_stream_execution_intercept( priority: i32, callable: JsFunction, ) -> Result<()> { - let key = PromiseAwareKey::GlobalLlmStreamExecution(name.clone()); let pa_fn = std::sync::Arc::new( crate::promise_call::PromiseAwareFn::new(&env, &callable).map_err(|e| { napi::Error::from_reason(format!("failed to create PromiseAwareFn: {e}")) @@ -3130,7 +3033,6 @@ pub fn register_llm_stream_execution_intercept( callable::wrap_js_llm_stream_exec_intercept_fn(pa_fn.clone()), ) .map_err(to_napi_err)?; - remember_promise_aware(key, pa_fn); Ok(()) } @@ -3139,13 +3041,7 @@ pub fn register_llm_stream_execution_intercept( /// Returns `true` if an intercept with that name was found and removed. #[napi] pub fn deregister_llm_stream_execution_intercept(name: String) -> Result { - let key = PromiseAwareKey::GlobalLlmStreamExecution(name.clone()); - let removed = - core_registry_api::deregister_llm_stream_execution_intercept(&name).map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + core_registry_api::deregister_llm_stream_execution_intercept(&name).map_err(to_napi_err) } // --------------------------------------------------------------------------- @@ -3458,10 +3354,6 @@ pub fn scope_register_tool_execution_intercept( )] callable: JsFunction, ) -> Result<()> { - let key = PromiseAwareKey::ScopeToolExecution { - scope_uuid: scope_uuid.clone(), - name: name.clone(), - }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; let pa_fn = std::sync::Arc::new( @@ -3476,7 +3368,6 @@ pub fn scope_register_tool_execution_intercept( callable::wrap_js_tool_exec_intercept_fn(pa_fn.clone()), ) .map_err(to_napi_err)?; - remember_promise_aware(key, pa_fn); Ok(()) } @@ -3485,18 +3376,9 @@ pub fn scope_register_tool_execution_intercept( /// Returns `true` if an intercept with that name was found and removed from the specified scope. #[napi] pub fn scope_deregister_tool_execution_intercept(scope_uuid: String, name: String) -> Result { - let key = PromiseAwareKey::ScopeToolExecution { - scope_uuid: scope_uuid.clone(), - name: name.clone(), - }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let removed = core_registry_api::scope_deregister_tool_execution_intercept(&uuid, &name) - .map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + core_registry_api::scope_deregister_tool_execution_intercept(&uuid, &name).map_err(to_napi_err) } // --------------------------------------------------------------------------- @@ -3697,10 +3579,6 @@ pub fn scope_register_llm_execution_intercept( priority: i32, callable: JsFunction, ) -> Result<()> { - let key = PromiseAwareKey::ScopeLlmExecution { - scope_uuid: scope_uuid.clone(), - name: name.clone(), - }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; let pa_fn = std::sync::Arc::new( @@ -3715,7 +3593,6 @@ pub fn scope_register_llm_execution_intercept( callable::wrap_js_llm_exec_intercept_fn(pa_fn.clone()), ) .map_err(to_napi_err)?; - remember_promise_aware(key, pa_fn); Ok(()) } @@ -3724,18 +3601,9 @@ pub fn scope_register_llm_execution_intercept( /// Returns `true` if an intercept with that name was found and removed from the specified scope. #[napi] pub fn scope_deregister_llm_execution_intercept(scope_uuid: String, name: String) -> Result { - let key = PromiseAwareKey::ScopeLlmExecution { - scope_uuid: scope_uuid.clone(), - name: name.clone(), - }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let removed = core_registry_api::scope_deregister_llm_execution_intercept(&uuid, &name) - .map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + core_registry_api::scope_deregister_llm_execution_intercept(&uuid, &name).map_err(to_napi_err) } /// Register a scope-local streaming LLM execution intercept following the middleware chain pattern. @@ -3752,10 +3620,6 @@ pub fn scope_register_llm_stream_execution_intercept( priority: i32, callable: JsFunction, ) -> Result<()> { - let key = PromiseAwareKey::ScopeLlmStreamExecution { - scope_uuid: scope_uuid.clone(), - name: name.clone(), - }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; let pa_fn = std::sync::Arc::new( @@ -3770,7 +3634,6 @@ pub fn scope_register_llm_stream_execution_intercept( callable::wrap_js_llm_stream_exec_intercept_fn(pa_fn.clone()), ) .map_err(to_napi_err)?; - remember_promise_aware(key, pa_fn); Ok(()) } @@ -3782,18 +3645,10 @@ pub fn scope_deregister_llm_stream_execution_intercept( scope_uuid: String, name: String, ) -> Result { - let key = PromiseAwareKey::ScopeLlmStreamExecution { - scope_uuid: scope_uuid.clone(), - name: name.clone(), - }; let uuid = uuid::Uuid::parse_str(&scope_uuid) .map_err(|e| napi::Error::from_reason(format!("invalid UUID: {e}")))?; - let removed = core_registry_api::scope_deregister_llm_stream_execution_intercept(&uuid, &name) - .map_err(to_napi_err)?; - if removed { - forget_promise_aware(&key); - } - Ok(removed) + core_registry_api::scope_deregister_llm_stream_execution_intercept(&uuid, &name) + .map_err(to_napi_err) } // --------------------------------------------------------------------------- diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index 40f780bc0..ea741558f 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -59,6 +59,27 @@ struct CallArgs { completion: CallCompletion, } +#[derive(Clone, Copy)] +struct CallMode { + spread: bool, + publication: bool, +} + +impl CallMode { + const DIRECT: Self = Self { + spread: false, + publication: false, + }; + const SPREAD: Self = Self { + spread: true, + publication: false, + }; + const SPREAD_PUBLICATION: Self = Self { + spread: true, + publication: true, + }; +} + #[derive(Clone)] struct CallCompletion { sender: Arc>>>>, @@ -234,7 +255,7 @@ impl PromiseAwareFn { /// Call the JS function with the given args and await the result. pub async fn call(&self, args: Json) -> FlowResult { - self.call_inner(PrimaryArg::Json(args), false, None, false) + self.call_inner(PrimaryArg::Json(args), CallMode::DIRECT, None) .await } @@ -244,14 +265,18 @@ impl PromiseAwareFn { /// guardrails, whose public contract is `(name, payload)` rather than a /// single envelope object. pub async fn call_spread(&self, args: Vec) -> FlowResult { - self.call_inner(PrimaryArg::Json(Json::Array(args)), true, None, false) + self.call_inner(PrimaryArg::Json(Json::Array(args)), CallMode::SPREAD, None) .await } /// Call a spread callback from queued event publication. pub async fn call_spread_for_publication(&self, args: Vec) -> FlowResult { - self.call_inner(PrimaryArg::Json(Json::Array(args)), true, None, true) - .await + self.call_inner( + PrimaryArg::Json(Json::Array(args)), + CallMode::SPREAD_PUBLICATION, + None, + ) + .await } /// Call the JS function with a builder-constructed first argument and await @@ -261,13 +286,13 @@ impl PromiseAwareFn { /// cannot cross the threadsafe-function boundary as plain JSON, such as a /// `#[napi]` class instance. pub async fn call_with_arg0(&self, build_arg0: Arg0Builder) -> FlowResult { - self.call_inner(PrimaryArg::Build(build_arg0), false, None, false) + self.call_inner(PrimaryArg::Build(build_arg0), CallMode::DIRECT, None) .await } /// Call a JavaScript callback with builder-constructed spread arguments. pub async fn call_spread_with_arg0(&self, build_arg0: Arg0Builder) -> FlowResult { - self.call_inner(PrimaryArg::Build(build_arg0), true, None, false) + self.call_inner(PrimaryArg::Build(build_arg0), CallMode::SPREAD, None) .await } @@ -276,8 +301,12 @@ impl PromiseAwareFn { &self, build_arg0: Arg0Builder, ) -> FlowResult { - self.call_inner(PrimaryArg::Build(build_arg0), true, None, true) - .await + self.call_inner( + PrimaryArg::Build(build_arg0), + CallMode::SPREAD_PUBLICATION, + None, + ) + .await } /// Call the JS function with a middleware-style `next(arg)` callback that @@ -285,9 +314,8 @@ impl PromiseAwareFn { pub async fn call_with_json_next(&self, args: Json, next: JsonNextFn) -> FlowResult { self.call_inner( PrimaryArg::Json(args), - false, + CallMode::DIRECT, Some(NextFn::Json(next)), - false, ) .await } @@ -301,9 +329,8 @@ impl PromiseAwareFn { ) -> FlowResult { self.call_inner( PrimaryArg::Json(args), - false, + CallMode::DIRECT, Some(NextFn::Stream(next)), - false, ) .await } @@ -318,9 +345,8 @@ impl PromiseAwareFn { async fn call_inner( &self, arg0: PrimaryArg, - spread: bool, + mode: CallMode, next: Option, - publication: bool, ) -> FlowResult { let (sender, receiver) = tokio::sync::oneshot::channel(); let tsfn = self @@ -333,9 +359,9 @@ impl PromiseAwareFn { let status = tsfn.call( Ok(CallArgs { arg0, - spread, + spread: mode.spread, next, - publication, + publication: mode.publication, completion: CallCompletion::new(sender), }), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, diff --git a/crates/node/tests/adaptive_tests.mjs b/crates/node/tests/adaptive_tests.mjs index bf2c4b090..ae45ceb79 100644 --- a/crates/node/tests/adaptive_tests.mjs +++ b/crates/node/tests/adaptive_tests.mjs @@ -200,6 +200,66 @@ describe('core plugins', () => { plugin.deregister(pluginKind); } }); + + it('snapshotted plugin execution intercepts survive configuration teardown', async () => { + const pluginKind = `node.test.execution-snapshot.${Date.now()}`; + let blockerEntered; + const entered = new Promise((resolve) => { + blockerEntered = resolve; + }); + let releaseBlocker; + const release = new Promise((resolve) => { + releaseBlocker = resolve; + }); + + plugin.register(pluginKind, { + register(_config, context) { + context.registerToolExecutionIntercept('target', 100, async (args, next) => ({ + result: { + ...(await next(args)), + snapshotted: true, + }, + })); + context.registerToolExecutionIntercept('blocker', -100, async (args, next) => { + blockerEntered(); + await release; + return { result: await next(args) }; + }); + }, + }); + + try { + await plugin.initialize({ + version: 1, + components: [ + plugin.ComponentSpec('observability', { + version: 3, + atof: { enabled: false }, + }), + adaptive.ComponentSpec({ + version: 1, + state: { backend: adaptive.inMemoryBackend() }, + adaptive_hints: adaptive.adaptiveHintsConfig(), + }), + plugin.ComponentSpec(pluginKind, {}), + ], + }); + const execution = lib.toolCallExecute('plugin_snapshot_tool', {}, () => ({ + downstream: true, + })); + await entered; + plugin.clear(); + releaseBlocker(); + assert.deepEqual(await execution, { + downstream: true, + snapshotted: true, + }); + } finally { + releaseBlocker(); + plugin.clear(); + plugin.deregister(pluginKind); + } + }); }); describe('adaptive helpers', () => { diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index 25fdccf63..596e8ad7b 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -58,6 +58,20 @@ async function flushSubscriberCallbacks() { } } +async function assertCompletesWithin(promise, message) { + let timeout; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timeout = setTimeout(() => reject(new assert.AssertionError({ message })), 2000); + }), + ]); + } finally { + clearTimeout(timeout); + } +} + function makeNative() { return { headers: {}, @@ -293,8 +307,9 @@ describe('LLM execute', () => { ); await waitForSubscriberCallbacks( - () => events.some((e) => e.name === 'exec_status_ok_llm' && e.scope_category === 'end') - && events.some((e) => e.name === 'exec_status_error_llm' && e.scope_category === 'end'), + () => + events.some((e) => e.name === 'exec_status_ok_llm' && e.scope_category === 'end') && + events.some((e) => e.name === 'exec_status_error_llm' && e.scope_category === 'end'), ); const okEnd = events.find( (e) => @@ -593,7 +608,10 @@ describe('LLM guardrails', () => { null, ); assert.deepEqual(await stream.next(), { delta: 'ok' }); - assert.equal(await stream.next(), null); + assert.equal( + await assertCompletesWithin(stream.next(), 'stream finalization deadlocked inside an async sanitizer'), + null, + ); await flushSubscribers(); } finally { deregisterLlmSanitizeResponseGuardrail('node_stream_flush_response'); @@ -696,11 +714,7 @@ describe('LLM guardrails', () => { try { const handle = llmCall('node_manual_flush', makeNative()); llmCallEnd(handle, { response: 'ok' }); - const outcome = await Promise.race([ - flushSubscribers().then(() => 'flushed'), - new Promise((resolve) => setTimeout(() => resolve('timeout'), 2000)), - ]); - assert.equal(outcome, 'flushed', 'flushSubscribers deadlocked inside an async sanitizer'); + await assertCompletesWithin(flushSubscribers(), 'flushSubscribers deadlocked inside an async sanitizer'); } finally { deregisterLlmSanitizeRequestGuardrail('node_manual_flush_request'); deregisterLlmSanitizeResponseGuardrail('node_manual_flush_response'); @@ -801,15 +815,14 @@ describe('LLM guardrails', () => { try { const request = makeNative(); await llmCallExecute('llm_san_req_throw', request, () => ({ ok: true }), null, null, null, null, null); - await waitForSubscriberCallbacks( - () => - events.some( - (event) => - event.name === 'llm_san_req_throw' && - event.kind === 'scope' && - event.category === 'llm' && - event.scope_category === 'start', - ), + await waitForSubscriberCallbacks(() => + events.some( + (event) => + event.name === 'llm_san_req_throw' && + event.kind === 'scope' && + event.category === 'llm' && + event.scope_category === 'start', + ), ); const start = events.find( (event) => @@ -981,7 +994,16 @@ describe('LLM guardrails', () => { return null; }); try { - const result = await llmCallExecute('llm_cond_promise', makeNative(), () => ({ ok: true }), null, null, null, null, null); + const result = await llmCallExecute( + 'llm_cond_promise', + makeNative(), + () => ({ ok: true }), + null, + null, + null, + null, + null, + ); assert.deepEqual(result, { ok: true }); } finally { deregisterLlmConditionalExecutionGuardrail('node_llm_cond_promise'); @@ -1472,10 +1494,7 @@ describe('LLM intercepts', () => { assert.equal(declarations.split(openKind).length - 1, 1); assert.equal(pluginDeclarations.split(openKind).length - 1, 1); - assert.match( - declarations, - /registerLlmRequestIntercept\([^\n]*import\('\.\/plugin'\)\.LlmRequestInterceptOutcome/, - ); + assert.match(declarations, /registerLlmRequestIntercept\([^\n]*import\('\.\/plugin'\)\.LlmRequestInterceptOutcome/); assert.match( declarations, /scopeRegisterLlmRequestIntercept\([^\n]*import\('\.\/plugin'\)\.LlmRequestInterceptOutcome/, @@ -1490,6 +1509,25 @@ describe('LLM intercepts', () => { assert.doesNotMatch(declarations, /registerLlmSanitizeRequestGuardrail\([^\n]*\.\.\.args: any\[\]/); }); + it('plugin declarations expose Promise middleware and the implemented stream contract', () => { + const declarations = readFileSync(new URL('../plugin.d.ts', import.meta.url), 'utf8'); + + assert.match(declarations, /registerMarkSanitizeGuardrail\([\s\S]*?Promise/); + assert.match(declarations, /registerScopeSanitizeStartGuardrail\([\s\S]*?Promise/); + assert.match(declarations, /registerScopeSanitizeEndGuardrail\([\s\S]*?Promise/); + assert.match(declarations, /registerToolSanitizeRequestGuardrail\([\s\S]*?Json \| Promise/); + assert.match(declarations, /registerToolSanitizeResponseGuardrail\([\s\S]*?Json \| Promise/); + assert.match(declarations, /registerLlmSanitizeRequestGuardrail\([\s\S]*?Promise/); + assert.match(declarations, /registerLlmSanitizeResponseGuardrail\([\s\S]*?Promise/); + assert.match(declarations, /registerLlmConditionalExecutionGuardrail\([\s\S]*?Promise/); + assert.match(declarations, /registerLlmRequestIntercept\([\s\S]*?Promise/); + assert.match( + declarations, + /registerLlmStreamExecutionIntercept\([\s\S]*?next: \(request: Json\) => Promise/, + ); + assert.doesNotMatch(declarations, /registerLlmStreamExecutionIntercept\([\s\S]*?AsyncIterable/); + }); + it('standalone conditional execution helper throws on rejection', async () => { registerLlmConditionalExecutionGuardrail('node_llm_cond_helper', 10, () => 'llm blocked by helper'); try { diff --git a/crates/node/tests/scope_local_tests.mjs b/crates/node/tests/scope_local_tests.mjs index e478a73b1..919bc2187 100644 --- a/crates/node/tests/scope_local_tests.mjs +++ b/crates/node/tests/scope_local_tests.mjs @@ -779,6 +779,54 @@ describe('Priority merge of global and scope-local middleware', () => { lib.deregisterToolExecutionIntercept('sl_merge_global_exec'); }); + it('snapshotted scope-local execution intercept survives deregistration', async () => { + const scope = pushScope('sl_snapshot_exec_scope', ScopeType.Agent, null, null); + let blockerEntered; + const entered = new Promise((resolve) => { + blockerEntered = resolve; + }); + let releaseBlocker; + const release = new Promise((resolve) => { + releaseBlocker = resolve; + }); + + scopeRegisterToolExecutionIntercept(scope.uuid, 'sl_snapshot_exec_target', 100, async (args, next) => ({ + result: { + ...(await next(args)), + snapshotted: true, + }, + })); + scopeRegisterToolExecutionIntercept(scope.uuid, 'sl_snapshot_exec_blocker', -100, async (args, next) => { + blockerEntered(); + await release; + return { result: await next(args) }; + }); + + try { + const execution = toolCallExecute( + 'sl_snapshot_exec_tool', + {}, + () => ({ downstream: true }), + null, + null, + null, + null, + ); + await entered; + assert.equal(scopeDeregisterToolExecutionIntercept(scope.uuid, 'sl_snapshot_exec_target'), true); + releaseBlocker(); + assert.deepEqual(await execution, { + downstream: true, + snapshotted: true, + }); + } finally { + releaseBlocker(); + scopeDeregisterToolExecutionIntercept(scope.uuid, 'sl_snapshot_exec_blocker'); + scopeDeregisterToolExecutionIntercept(scope.uuid, 'sl_snapshot_exec_target'); + popScope(scope); + } + }); + it('global and scope-local llm request intercepts both run with priority ordering', async () => { const order = []; diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index 4576d8253..1fe913fd8 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -735,9 +735,7 @@ describe('Tool guardrails', () => { } assert.equal(requestFlushed, true); assert.equal(responseFlushed, true); - const start = events.find( - (event) => event.name === 'node_manual_tool_flush' && event.scope_category === 'start', - ); + const start = events.find((event) => event.name === 'node_manual_tool_flush' && event.scope_category === 'start'); const end = events.find((event) => event.name === 'node_manual_tool_flush' && event.scope_category === 'end'); assert.deepEqual(start.data, { original: true, requestSanitized: true }); assert.deepEqual(end.data, { ok: true, responseSanitized: true }); @@ -964,6 +962,44 @@ describe('Tool intercepts', () => { } }); + it('snapshotted execution intercept survives deregistration', async () => { + let blockerEntered; + const entered = new Promise((resolve) => { + blockerEntered = resolve; + }); + let releaseBlocker; + const release = new Promise((resolve) => { + releaseBlocker = resolve; + }); + + registerToolExecutionIntercept('node_tool_exec_snapshot_target', 100, async (args, next) => ({ + result: { + ...(await next(args)), + snapshotted: true, + }, + })); + registerToolExecutionIntercept('node_tool_exec_snapshot_blocker', -100, async (args, next) => { + blockerEntered(); + await release; + return { result: await next(args) }; + }); + + try { + const execution = toolCallExecute('snapshotted_tool', {}, () => ({ downstream: true }), null, null, null, null); + await entered; + assert.equal(deregisterToolExecutionIntercept('node_tool_exec_snapshot_target'), true); + releaseBlocker(); + assert.deepEqual(await execution, { + downstream: true, + snapshotted: true, + }); + } finally { + releaseBlocker(); + deregisterToolExecutionIntercept('node_tool_exec_snapshot_blocker'); + deregisterToolExecutionIntercept('node_tool_exec_snapshot_target'); + } + }); + it('execution intercept propagates Error messages', async () => { registerToolExecutionIntercept('node_tool_exec_throw', 10, async () => { throw new Error('tool middleware exploded'); diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 1e08c05f7..a29d56131 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -860,6 +860,35 @@ pub struct NemoRelayNativeAsyncNext { _marker: PhantomData<(*mut u8, PhantomPinned)>, } +/// Opaque incremental output channel supplied to native async stream intercepts. +#[repr(C)] +pub struct NemoRelayNativeAsyncStream { + _private: [u8; 0], + _marker: PhantomData<(*mut u8, PhantomPinned)>, +} + +/// Receives one downstream stream item. `chunk_json` is non-null for a chunk, +/// `error` is non-null for failure, and `done` marks clean completion. Return +/// `false` to cancel downstream production after the current callback. +pub type NemoRelayNativeAsyncNextStreamCb = unsafe extern "C" fn( + user_data: *mut c_void, + chunk_json: *const NemoRelayNativeString, + error: *const NemoRelayNativeString, + done: bool, +) -> bool; + +/// Incremental native LLM stream intercept callback. +/// +/// The callback owns `next` and `stream` and must release each exactly once. +/// It may push chunks before returning or retain the handles and return +/// `Pending`; no implicit timeout is applied. +pub type NemoRelayNativeAsyncStreamMiddlewareCb = unsafe extern "C" fn( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + next: *const NemoRelayNativeAsyncNext, + stream: *const NemoRelayNativeAsyncStream, +) -> u32; + /// Completion-based native middleware callback. /// /// `invocation_json` is borrowed for the call. A callback that returns @@ -867,7 +896,9 @@ pub struct NemoRelayNativeAsyncNext { /// completion reference and must settle it then call the v3 /// `async_completion_release` hook. The host validates the returned /// discriminant. When `next` is non-null, the callback owns that handle for -/// the invocation and must call `async_next_release` after its final use. +/// the invocation and must call `async_next_release` exactly once after its +/// final use, regardless of whether it returns `Complete` or `Pending`. The +/// host never reclaims a `next` handle after handing it to the callback. /// `next` is null for non-execution middleware. pub type NemoRelayNativeAsyncMiddlewareCb = unsafe extern "C" fn( user_data: *mut c_void, @@ -907,7 +938,10 @@ pub struct NemoRelayNativeHostApiV3 { invocation_json: *const NemoRelayNativeString, completion: *const NemoRelayNativeAsyncCompletion, ) -> NemoRelayStatus, - /// Releases the callback-owned continuation reference for a pending callback. + /// Releases the callback-owned continuation reference. + /// + /// Execution callbacks must call this exactly once after their final use + /// for both `Complete` and `Pending` return states. pub async_next_release: unsafe extern "C" fn(next: *const NemoRelayNativeAsyncNext), /// Registers any completion-based asynchronous middleware surface. /// @@ -923,6 +957,42 @@ pub struct NemoRelayNativeHostApiV3 { user_data: *mut c_void, free_fn: NemoRelayNativeFreeFn, ) -> NemoRelayStatus, + /// Pushes one JSON chunk to an incremental native stream. + pub async_stream_push_json: unsafe extern "C" fn( + stream: *const NemoRelayNativeAsyncStream, + chunk_json: *const NemoRelayNativeString, + ) -> NemoRelayStatus, + /// Finishes an incremental native stream successfully. + pub async_stream_finish: + unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> NemoRelayStatus, + /// Rejects an incremental native stream. + pub async_stream_reject: unsafe extern "C" fn( + stream: *const NemoRelayNativeAsyncStream, + message: *const NemoRelayNativeString, + ) -> NemoRelayStatus, + /// Returns true when the consumer cancelled or released the stream. + pub async_stream_is_cancelled: + unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> bool, + /// Releases the callback-owned incremental stream reference. + pub async_stream_release: unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream), + /// Invokes a downstream stream and reports chunks incrementally. + pub async_next_invoke_stream: unsafe extern "C" fn( + next: *const NemoRelayNativeAsyncNext, + invocation_json: *const NemoRelayNativeString, + stream: *const NemoRelayNativeAsyncStream, + cb: NemoRelayNativeAsyncNextStreamCb, + user_data: *mut c_void, + ) -> NemoRelayStatus, + /// Registers an incremental asynchronous LLM stream intercept. + pub plugin_context_register_async_stream_middleware: unsafe extern "C" fn( + ctx: *mut NemoRelayNativePluginContext, + name: *const NemoRelayNativeString, + priority: i32, + cb: NemoRelayNativeAsyncStreamMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, + ) + -> NemoRelayStatus, } unsafe impl Send for NemoRelayNativeHostApiV3 {} @@ -2446,6 +2516,33 @@ impl<'a> PluginContext<'a> { }) } + /// Registers an incremental completion-based LLM stream intercept. + /// + /// # Safety + /// The callback and user data must remain valid until deregistration or + /// `free_fn`; callback-owned `next` and `stream` handles must each be + /// released exactly once. + pub unsafe fn register_async_stream_middleware_raw( + &mut self, + name: &str, + priority: i32, + cb: NemoRelayNativeAsyncStreamMiddlewareCb, + user_data: *mut c_void, + free_fn: NemoRelayNativeFreeFn, + ) -> NemoRelayStatus { + if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE + || self.host.struct_size < std::mem::size_of::() + { + return NemoRelayStatus::InvalidArg; + } + let host = unsafe { &*(self.host as *const _ as *const NemoRelayNativeHostApiV3) }; + self.with_name(name, |_, name| unsafe { + (host.plugin_context_register_async_stream_middleware)( + self.raw, name, priority, cb, user_data, free_fn, + ) + }) + } + fn with_name( &self, name: &str, diff --git a/crates/plugin/tests/typed_callbacks.rs b/crates/plugin/tests/typed_callbacks.rs index a56b2fdc6..c6e81d314 100644 --- a/crates/plugin/tests/typed_callbacks.rs +++ b/crates/plugin/tests/typed_callbacks.rs @@ -362,10 +362,12 @@ fn native_abi_v3_struct_sizes_are_self_describing() { ] ); assert_eq!(align_of::(), 8); - assert_eq!(size_of::(), 376); + assert_eq!(size_of::(), 432); assert_eq!( host_api_v3_offsets(), - [0, 320, 328, 336, 344, 352, 360, 368] + [ + 0, 320, 328, 336, 344, 352, 360, 368, 376, 384, 392, 400, 408, 416, 424 + ] ); assert_eq!(align_of::(), 8); assert_eq!(size_of::(), 56); @@ -388,10 +390,12 @@ fn native_abi_v3_struct_sizes_are_self_describing() { ] ); assert_eq!(align_of::(), 4); - assert_eq!(size_of::(), 188); + assert_eq!(size_of::(), 216); assert_eq!( host_api_v3_offsets(), - [0, 160, 164, 168, 172, 176, 180, 184] + [ + 0, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212 + ] ); assert_eq!(align_of::(), 4); assert_eq!(size_of::(), 28); @@ -402,7 +406,7 @@ fn native_abi_v3_struct_sizes_are_self_describing() { } } -fn host_api_v3_offsets() -> [usize; 8] { +fn host_api_v3_offsets() -> [usize; 15] { [ offset_of!(NemoRelayNativeHostApiV3, v1), offset_of!(NemoRelayNativeHostApiV3, async_completion_resolve_json), @@ -415,6 +419,16 @@ fn host_api_v3_offsets() -> [usize; 8] { NemoRelayNativeHostApiV3, plugin_context_register_async_middleware ), + offset_of!(NemoRelayNativeHostApiV3, async_stream_push_json), + offset_of!(NemoRelayNativeHostApiV3, async_stream_finish), + offset_of!(NemoRelayNativeHostApiV3, async_stream_reject), + offset_of!(NemoRelayNativeHostApiV3, async_stream_is_cancelled), + offset_of!(NemoRelayNativeHostApiV3, async_stream_release), + offset_of!(NemoRelayNativeHostApiV3, async_next_invoke_stream), + offset_of!( + NemoRelayNativeHostApiV3, + plugin_context_register_async_stream_middleware + ), ] } diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 53f48f33e..911f4b694 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -8,6 +8,8 @@ //! The Python wrapper modules (`nemo_relay.scope`, `nemo_relay.tools`, etc.) //! re-export these under shorter, idiomatic names. +use std::future::Future; +use std::panic::resume_unwind; use std::sync::Arc; use nemo_relay::api::llm as core_llm_api; @@ -55,6 +57,63 @@ fn to_py_err(e: FlowError) -> PyErr { PyErr::new::(e.to_string()) } +fn python_event_loop_running(py: Python<'_>) -> PyResult { + match py.import("asyncio")?.call_method0("get_running_loop") { + Ok(_) => Ok(true), + Err(error) if error.is_instance_of::(py) => Ok(false), + Err(error) => Err(error), + } +} + +fn block_on_sync_middleware(future: F) -> FlowResult +where + F: Future> + Send, + T: Send, +{ + let runtime = pyo3_async_runtimes::tokio::get_runtime(); + if tokio::runtime::Handle::try_current().is_ok() { + std::thread::scope(|scope| { + scope + .spawn(move || runtime.block_on(future)) + .join() + .unwrap_or_else(|panic| resume_unwind(panic)) + }) + } else { + runtime.block_on(future) + } +} + +fn run_standalone_middleware<'py, F, T, C>( + py: Python<'py>, + future: F, + convert: C, +) -> PyResult> +where + F: Future> + Send + 'static, + T: Send + 'static, + C: Fn(Python<'_>, T) -> PyResult> + Send + 'static, +{ + let scope_stack = current_scope_stack_handle(); + if !python_event_loop_running(py)? { + let result = py + .detach(|| { + block_on_sync_middleware( + py_callable::PY_AWAITABLES_ALLOWED + .scope(false, TASK_SCOPE_STACK.scope(scope_stack, future)), + ) + }) + .map_err(to_py_err)?; + return convert(py, result).map(|value| value.into_bound(py)); + } + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let result = TASK_SCOPE_STACK + .scope(scope_stack, future) + .await + .map_err(to_py_err)?; + Python::attach(|py| convert(py, result)) + }) +} + fn py_llm_response_codec( response_codec: Option<&Bound<'_, PyAny>>, ) -> Option> { @@ -1317,41 +1376,11 @@ fn tool_request_intercepts<'py>( args: &Bound<'py, PyAny>, ) -> PyResult> { let args_json = py_to_json(args)?; - // Preserve the established synchronous helper behavior when no Python - // event loop is active. Awaitable middleware is supported from async - // callers below; a synchronous caller can continue using direct - // callbacks without manufacturing an asyncio loop. - if py - .import("asyncio")? - .call_method0("get_running_loop") - .is_err() - { - let scope_stack = current_scope_stack_handle(); - let result = py - .detach(|| { - pyo3_async_runtimes::tokio::get_runtime().block_on( - py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_tool_api::tool_request_intercepts(&name, args_json).await - }), - ), - ) - }) - .map_err(to_py_err)?; - return json_to_py(py, &result).map(|value| value.into_bound(py)); - } - let scope_stack = current_scope_stack_handle(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let result = core_tool_api::tool_request_intercepts(&name, args_json) - .await - .map_err(to_py_err)?; - Python::attach(|py| json_to_py(py, &result)) - }) - .await - }) + run_standalone_middleware( + py, + async move { core_tool_api::tool_request_intercepts(&name, args_json).await }, + |py, result| json_to_py(py, &result), + ) } /// Run the registered tool conditional execution guardrail chain. @@ -1368,35 +1397,11 @@ fn tool_conditional_execution<'py>( args: &Bound<'py, PyAny>, ) -> PyResult> { let args_json = py_to_json(args)?; - if py - .import("asyncio")? - .call_method0("get_running_loop") - .is_err() - { - let scope_stack = current_scope_stack_handle(); - py.detach(|| { - pyo3_async_runtimes::tokio::get_runtime().block_on( - py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_tool_api::tool_conditional_execution(&name, &args_json).await - }), - ), - ) - }) - .map_err(to_py_err)?; - return Ok(py.None().into_bound(py)); - } - let scope_stack = current_scope_stack_handle(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_conditional_execution(&name, &args_json) - .await - .map_err(to_py_err) - }) - .await - }) + run_standalone_middleware( + py, + async move { core_tool_api::tool_conditional_execution(&name, &args_json).await }, + |py, ()| Ok(py.None()), + ) } /// Run the registered LLM request intercept chain on the given request. @@ -1414,41 +1419,17 @@ fn llm_request_intercepts<'py>( name: String, request: PyLLMRequest, ) -> PyResult> { - if py - .import("asyncio")? - .call_method0("get_running_loop") - .is_err() - { - let scope_stack = current_scope_stack_handle(); - let result = py - .detach(|| { - pyo3_async_runtimes::tokio::get_runtime().block_on( - py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_llm_api::llm_request_intercepts(&name, request.inner).await - }), - ), - ) - }) - .map_err(to_py_err)?; - return Py::new( - py, - crate::py_types::PyLLMRequestInterceptOutcome { inner: result }, - ) - .map(|value| value.into_bound(py).into_any()); - } - let scope_stack = current_scope_stack_handle(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let result = core_llm_api::llm_request_intercepts(&name, request.inner) - .await - .map_err(to_py_err)?; - Ok(crate::py_types::PyLLMRequestInterceptOutcome { inner: result }) - }) - .await - }) + run_standalone_middleware( + py, + async move { core_llm_api::llm_request_intercepts(&name, request.inner).await }, + |py, result| { + Py::new( + py, + crate::py_types::PyLLMRequestInterceptOutcome { inner: result }, + ) + .map(Py::into_any) + }, + ) } /// Run the registered LLM conditional execution guardrail chain. @@ -1462,35 +1443,11 @@ fn llm_conditional_execution<'py>( py: Python<'py>, request: PyLLMRequest, ) -> PyResult> { - if py - .import("asyncio")? - .call_method0("get_running_loop") - .is_err() - { - let scope_stack = current_scope_stack_handle(); - py.detach(|| { - pyo3_async_runtimes::tokio::get_runtime().block_on( - py_callable::PY_AWAITABLES_ALLOWED.scope( - false, - TASK_SCOPE_STACK.scope(scope_stack, async move { - core_llm_api::llm_conditional_execution(&request.inner).await - }), - ), - ) - }) - .map_err(to_py_err)?; - return Ok(py.None().into_bound(py)); - } - let scope_stack = current_scope_stack_handle(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_llm_api::llm_conditional_execution(&request.inner) - .await - .map_err(to_py_err) - }) - .await - }) + run_standalone_middleware( + py, + async move { core_llm_api::llm_conditional_execution(&request.inner).await }, + |py, ()| Ok(py.None()), + ) } // --------------------------------------------------------------------------- diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index a947d628b..8260af6c6 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -108,6 +108,31 @@ fn split_json_or_future( } } +fn split_json_or_future_with_locals( + py: Python<'_>, + result: Py, + task_locals: Option<&TaskLocals>, +) -> FlowResult> { + let bound = result.bind(py); + if bound.getattr("__await__").is_ok() { + reject_awaitable_from_sync_caller(bound)?; + let future: PyValueFuture = match task_locals { + Some(locals) => Box::pin( + pyo3_async_runtimes::into_future_with_locals(locals, result.into_bound(py)) + .map_err(|error| FlowError::Internal(error.to_string()))?, + ), + None => Box::pin( + pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) + .map_err(|error| FlowError::Internal(error.to_string()))?, + ), + }; + Ok(Err(future)) + } else { + let json = py_to_json(bound).map_err(|error| FlowError::Internal(error.to_string()))?; + Ok(Ok(json)) + } +} + async fn resolve_json_or_future( outcome: FlowResult>, ) -> FlowResult { @@ -519,8 +544,10 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { /// Wrap a Python callable `(str, Json) -> Optional[str]` for tool conditional guardrails. pub fn wrap_py_tool_conditional_fn(py_fn: Py) -> ToolConditionalFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new(move |name: String, args: Json| { let py_fn = py_fn.clone(); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let py_args = @@ -528,7 +555,7 @@ pub fn wrap_py_tool_conditional_fn(py_fn: Py) -> ToolConditionalFn { let result = py_fn .call1(py, (name, py_args)) .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future(py, result) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) })) .await?; Python::attach(|py| { @@ -550,8 +577,10 @@ pub fn wrap_py_tool_conditional_fn(py_fn: Py) -> ToolConditionalFn { /// Wrap a Python callable `(str, Json) -> Json` for tool request intercepts. pub fn wrap_py_tool_request_intercept_fn(py_fn: Py) -> ToolInterceptFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new(move |name: String, args: Json| { let py_fn = py_fn.clone(); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { resolve_json_or_future(Python::attach(|py| { let py_args = @@ -559,7 +588,7 @@ pub fn wrap_py_tool_request_intercept_fn(py_fn: Py) -> ToolInterceptFn { let result = py_fn .call1(py, (name, py_args)) .map_err(|e| FlowError::Internal(e.to_string()))?; - split_json_or_future(py, result) + split_json_or_future_with_locals(py, result, task_locals.as_ref()) })) .await }) @@ -987,9 +1016,11 @@ pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { /// edits must be made through the returned annotation; headers remain writable. pub fn wrap_py_llm_request_intercept_fn(py_fn: Py) -> LlmRequestInterceptFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new( move |name: String, request: LlmRequest, annotated: Option| { let py_fn = py_fn.clone(); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let py_req = PyLLMRequest { inner: request }; @@ -1012,7 +1043,7 @@ pub fn wrap_py_llm_request_intercept_fn(py_fn: Py) -> LlmRequestIntercept FlowError::Internal(format!("LLM request intercept callable failed: {e}")) })?; - split_py_object_or_future(py, result) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) })) .await?; Python::attach(|py| { diff --git a/crates/python/src/test_support.rs b/crates/python/src/test_support.rs index 662d6c255..e4986103a 100644 --- a/crates/python/src/test_support.rs +++ b/crates/python/src/test_support.rs @@ -8,6 +8,7 @@ use pyo3::Python; const BINDING_KIND_ENV: &str = "NEMO_RELAY_BINDING_KIND"; const RUNTIME_OWNER_ENV: &str = "NEMO_RELAY_RUNTIME_OWNER"; +const XDG_CONFIG_HOME_ENV: &str = "XDG_CONFIG_HOME"; fn python_test_lock() -> &'static Mutex<()> { static PYTHON_TEST_LOCK: OnceLock> = OnceLock::new(); @@ -31,6 +32,7 @@ pub(crate) struct PythonTestGuard { _lock: MutexGuard<'static, ()>, binding_kind: Option, runtime_owner: Option, + xdg_config_home: Option, } impl Drop for PythonTestGuard { @@ -44,6 +46,10 @@ impl Drop for PythonTestGuard { Some(value) => std::env::set_var(BINDING_KIND_ENV, value), None => std::env::remove_var(BINDING_KIND_ENV), }; + match &self.xdg_config_home { + Some(value) => std::env::set_var(XDG_CONFIG_HOME_ENV, value), + None => std::env::remove_var(XDG_CONFIG_HOME_ENV), + }; } } } @@ -51,12 +57,19 @@ impl Drop for PythonTestGuard { pub(crate) fn init_python_test_locked(lock: MutexGuard<'static, ()>) -> PythonTestGuard { let binding_kind = std::env::var_os(BINDING_KIND_ENV); let runtime_owner = std::env::var_os(RUNTIME_OWNER_ENV); + let xdg_config_home = std::env::var_os(XDG_CONFIG_HOME_ENV); clear_runtime_owner_env(); + let isolated_config_home = + std::env::temp_dir().join(format!("nemo-relay-python-tests-{}", std::process::id())); + unsafe { + std::env::set_var(XDG_CONFIG_HOME_ENV, isolated_config_home); + } Python::initialize(); PythonTestGuard { _lock: lock, binding_kind, runtime_owner, + xdg_config_home, } } diff --git a/crates/python/tests/coverage/py_api_coverage_tests.rs b/crates/python/tests/coverage/py_api_coverage_tests.rs index 45df22794..8f3b0ee5e 100644 --- a/crates/python/tests/coverage/py_api_coverage_tests.rs +++ b/crates/python/tests/coverage/py_api_coverage_tests.rs @@ -917,6 +917,18 @@ fn to_py_err_and_forward_stream_to_channel_cover_private_helpers() { }); } +#[test] +fn synchronous_middleware_bridge_avoids_tokio_runtime_reentry() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let result = runtime.block_on(async { + block_on_sync_middleware(async { Ok::<_, nemo_relay::error::FlowError>(7) }) + }); + assert_eq!(result.unwrap(), 7); +} + #[test] fn llm_execution_uses_all_response_codec_selection_paths() { let _python = crate::test_support::init_python_test(); diff --git a/docs/instrument-applications/advanced-guide.mdx b/docs/instrument-applications/advanced-guide.mdx index 38e05d264..70220e456 100644 --- a/docs/instrument-applications/advanced-guide.mdx +++ b/docs/instrument-applications/advanced-guide.mdx @@ -198,11 +198,12 @@ Sanitize guardrails affect only the payload recorded on emitted events. Request ### Node.js Callback Failures -Node.js guardrail and request-intercept callbacks are synchronous. A thrown -conditional-execution guardrail or request intercept rejects the managed call -before protected execution or later middleware runs. A thrown sanitize guardrail -or event sanitizer fails open, preserving the current emitted payload and -recording the error for `getLastCallbackError()`. +Node.js guardrail and request-intercept callbacks can return direct values or +Promises. A thrown error or rejected Promise from a conditional-execution +guardrail or request intercept rejects the managed call before protected +execution or later middleware runs. A thrown error or rejected Promise from a +sanitize guardrail or event sanitizer fails open, preserving the current emitted +payload and recording the error for `getLastCallbackError()`. Scope-local variants are available through `nemo_relay.scope_local.register_*`, Node.js `scopeRegister*` helpers, and Rust `scope_register_*` functions. diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index cb09524e6..231d1b65c 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -100,6 +100,12 @@ Only enqueue-time validation and runtime-state errors are returned directly; middleware and codec errors discovered during queued publication are logged and handled according to their fail-open contracts. +Python subscriber flushing is context-sensitive. Continue to call +`nemo_relay.subscribers.flush()` from synchronous code. From a running +`asyncio` event loop, replace that call with +`await nemo_relay.subscribers.flush_async()` so queued async middleware can +continue running; the synchronous flush raises in that context. + ### Update LLM Sanitizer Callbacks The registration names remain unchanged for global, plugin-context, and diff --git a/python/nemo_relay/__init__.py b/python/nemo_relay/__init__.py index 1b6a3a9ee..7d88c1b14 100644 --- a/python/nemo_relay/__init__.py +++ b/python/nemo_relay/__init__.py @@ -165,8 +165,8 @@ class EventSanitizeFields(TypedDict): #: Guardrail callback that sanitizes emitted tool request or response payloads. #: Arguments are the tool name and JSON payload. The return value is the JSON -#: payload recorded on the emitted event. Exceptions propagate through the -#: lifecycle call that invoked the guardrail. +#: payload recorded on the emitted event. Exceptions fail open and preserve the +#: last valid observability payload. ToolSanitizeGuardrail: TypeAlias = Callable[[str, Json], Json | Awaitable[Json]] EventSanitizeGuardrail: TypeAlias = Callable[ ["Event", EventSanitizeFields], EventSanitizeFields | Awaitable[EventSanitizeFields] diff --git a/python/nemo_relay/plugin.py b/python/nemo_relay/plugin.py index c143f29c6..8cc927f36 100644 --- a/python/nemo_relay/plugin.py +++ b/python/nemo_relay/plugin.py @@ -489,9 +489,11 @@ async def plugin(config: PluginConfig | JsonObject, *, clear_on_exit: bool = Tru try: yield report_ finally: - subscribers.flush() - if clear_on_exit: - clear() + try: + await subscribers.flush_async() + finally: + if clear_on_exit: + clear() def report() -> ConfigReport | None: diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index 9762d3ab6..f0fb72339 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -24,6 +24,7 @@ def log_event(event): import asyncio from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor from typing import TYPE_CHECKING from nemo_relay._event_sanitizer_context import callback_active as _event_sanitizer_callback_active @@ -40,6 +41,8 @@ def log_event(event): if TYPE_CHECKING: from nemo_relay import Event +_FLUSH_EXECUTOR = ThreadPoolExecutor(max_workers=1, thread_name_prefix="nemo-relay-flush") + def register(name: str, callback: "Callable[[Event], None]") -> None: """Register a global event subscriber. @@ -122,12 +125,13 @@ async def flush_async() -> None: """Wait asynchronously for subscriber callbacks already queued by Relay. Use this barrier from an ``asyncio`` task. The blocking native wait runs on - a worker thread so an event sanitizer scheduled on the caller's event loop - can continue to make progress. + Relay's dedicated flush thread so an event sanitizer scheduled on the + caller's event loop or default executor can continue to make progress. """ if _event_sanitizer_callback_active(): return None - await asyncio.to_thread(_native_flush) + loop = asyncio.get_running_loop() + await loop.run_in_executor(_FLUSH_EXECUTOR, _native_flush) __all__ = ["deregister", "flush", "flush_async", "register"] diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 788f4dce1..2e14ebc73 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -7,6 +7,7 @@ import typing from collections.abc import Iterator +from pathlib import Path from uuid import uuid4 import pytest @@ -15,6 +16,12 @@ import nemo_relay +@pytest.fixture(autouse=True) +def isolate_user_plugin_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + """Prevent local user plugin configuration from affecting test behavior.""" + monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg-config")) + + @pytest.fixture(name="subscribed_events") def subscribed_events_fixture() -> Iterator[list[nemo_relay.Event]]: import nemo_relay diff --git a/python/tests/integrations/langgraph_tests/test_langgraph_integration.py b/python/tests/integrations/langgraph_tests/test_langgraph_integration.py index cd2decb73..7005253d8 100644 --- a/python/tests/integrations/langgraph_tests/test_langgraph_integration.py +++ b/python/tests/integrations/langgraph_tests/test_langgraph_integration.py @@ -118,7 +118,7 @@ async def test_async( with nemo_relay.scope.scope("request", nemo_relay.ScopeType.Agent): result = await async_graph.ainvoke({"value": 1}, config={"callbacks": [callback_handler]}) - nemo_relay.subscribers.flush() + await nemo_relay.subscribers.flush_async() assert result == {"value": 2} assert _events_to_strings(subscribed_events) == self._expected_events diff --git a/python/tests/test_builtin_codecs.py b/python/tests/test_builtin_codecs.py index c1fcef898..2e794a0a5 100644 --- a/python/tests/test_builtin_codecs.py +++ b/python/tests/test_builtin_codecs.py @@ -498,7 +498,7 @@ async def mock_llm(req): mock_llm, response_codec=codec, ) - subscribers.flush() + await subscribers.flush_async() # Find LLMEnd event end_events = [ @@ -531,7 +531,7 @@ async def mock_llm(req): return {"result": "ok"} await llm.execute("test-llm", request, mock_llm) - subscribers.flush() + await subscribers.flush_async() end_events = [ e for e in captured_events if e.kind == "scope" and e.category == "llm" and e.scope_category == "end" diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index b002972de..1aadb30a1 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Iterator +from concurrent.futures import ThreadPoolExecutor from typing import cast import pytest @@ -90,7 +91,7 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve guardrails.register_mark_sanitize("python-async-mark", 0, sanitize) try: scope.event("async-checkpoint", data={"raw": True}) - await asyncio.to_thread(subscribers.flush) + await subscribers.flush_async() finally: guardrails.deregister_mark_sanitize("python-async-mark") @@ -120,6 +121,28 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert events[-1].data == {"async_flush": True} +async def test_async_flush_does_not_consume_default_executor(capture_events): + _capture_name, events = capture_events + asyncio.get_running_loop().set_default_executor(ThreadPoolExecutor(max_workers=1)) + + async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + await asyncio.to_thread(lambda: None) + return { + "data": {"default_executor": True}, + "category_profile": fields["category_profile"], + "metadata": fields["metadata"], + } + + guardrails.register_mark_sanitize("python-async-flush-executor", 0, sanitize) + try: + scope.event("async-flush-executor-checkpoint", data={"raw": True}) + await asyncio.wait_for(subscribers.flush_async(), timeout=2) + finally: + guardrails.deregister_mark_sanitize("python-async-flush-executor") + + assert events[-1].data == {"default_executor": True} + + def test_async_sanitizer_registered_on_closed_loop_uses_fallback(capture_events): _capture_name, events = capture_events @@ -166,7 +189,7 @@ async def sanitize_async(_event: nemo_relay.Event, fields: EventSanitizeFields) ) try: scope.event("reentrant-checkpoint", data={"raw": True}) - await asyncio.to_thread(subscribers.flush) + await subscribers.flush_async() finally: guardrails.deregister_mark_sanitize("python-reentrant-mark") @@ -256,10 +279,10 @@ def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSani try: await plugin.initialize(plugin.PluginConfig(components=[plugin.ComponentSpec(kind=kind)])) scope.event("configured", data={"raw": True}) - subscribers.flush() + await subscribers.flush_async() plugin.clear() scope.event("cleared", data={"raw": True}) - subscribers.flush() + await subscribers.flush_async() finally: plugin.clear() plugin.deregister(kind) @@ -292,7 +315,7 @@ def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSani with pytest.raises(RuntimeError, match="registration failed"): await plugin.initialize(plugin.PluginConfig(components=[plugin.ComponentSpec(kind=kind)])) scope.event("after-failure", data={"raw": True}) - subscribers.flush() + await subscribers.flush_async() assert events[-1].data == {"raw": True} finally: plugin.clear() diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index 5bf315f3e..0ed04bde7 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -205,7 +205,7 @@ async def sanitize_response(response, context) -> dict: try: handle = llm.call("py_manual_flush", make_request()) llm.call_end(handle, {"response": "ok"}) - await asyncio.wait_for(asyncio.to_thread(subscribers.flush), timeout=2) + await asyncio.wait_for(subscribers.flush_async(), timeout=2) finally: guardrails.deregister_llm_sanitize_request("py_manual_flush_request") guardrails.deregister_llm_sanitize_response("py_manual_flush_response") @@ -558,6 +558,27 @@ def test_deregister_nonexistent(self): class TestLLMInterceptsAsync: + async def test_async_request_intercept_runs_on_originating_loop(self): + originating_loop = asyncio.get_running_loop() + + async def intercept_fn(_name, request, annotated): + await asyncio.sleep(0) + assert asyncio.get_running_loop() is originating_loop + content = {**request.content, "intercepted": True} + return LLMRequestInterceptOutcome(LLMRequest(request.headers, content), annotated) + + intercepts.register_llm_request("py_llm_async_request_loop", 1, False, intercept_fn) + try: + result = await llm.execute( + "async_request_llm", + make_request(), + lambda request: {"intercepted": request.content["intercepted"]}, + ) + finally: + intercepts.deregister_llm_request("py_llm_async_request_loop") + + assert result == {"intercepted": True} + async def test_request_intercept_modifies(self): def intercept_fn(name, request, annotated): # Request intercepts now operate on LLMRequest @@ -726,7 +747,7 @@ async def stream_func(request) -> AsyncIterator[dict]: lambda: {"raw": True}, ) assert [chunk async for chunk in stream] == [{"token": "hello"}] - await asyncio.to_thread(subscribers.flush) + await subscribers.flush_async() finally: guardrails.deregister_llm_sanitize_response("py_llm_async_stream_sanitizer") subscribers.deregister("py_llm_async_stream_sanitizer_sub") @@ -906,7 +927,7 @@ async def gen(): assert chunks == [{"token": "hello"}] finally: try: - subscribers.flush() + await subscribers.flush_async() finally: subscribers.deregister("py_llm_finalizer_fail_sub") @@ -937,7 +958,7 @@ async def gen(): assert chunks == [{"token": "hello"}] finally: try: - subscribers.flush() + await subscribers.flush_async() finally: subscribers.deregister("py_llm_finalizer_callable_fail_sub") @@ -953,7 +974,7 @@ async def test_subscriber_exception_does_not_break_streaming(self): llm.call_end(handle, {"ok": True}) finally: try: - subscribers.flush() + await subscribers.flush_async() finally: subscribers.deregister("py_llm_bad_sub") subscribers.deregister("py_llm_good_sub") diff --git a/python/tests/test_scope_local.py b/python/tests/test_scope_local.py index d20c7382d..e0601dd35 100644 --- a/python/tests/test_scope_local.py +++ b/python/tests/test_scope_local.py @@ -72,7 +72,7 @@ def my_tool(args): scope_local.register_tool_sanitize_request(handle, "sl_sanitizer", 1, sanitizer) scope_local.register_subscriber(handle, "sl_sanitizer_sub", lambda e: events.append(e)) result = await tools.execute("sanitized_tool", {"input": "data"}, my_tool) - subscribers.flush() + await subscribers.flush_async() # Sanitize guardrails are observability-only: they do NOT modify args # flowing through the execution pipeline. @@ -98,7 +98,7 @@ def my_tool(args): scope_local.register_tool_sanitize_response(handle, "sl_resp_sanitizer", 1, response_sanitizer) scope_local.register_subscriber(handle, "sl_resp_sub", lambda e: events.append(e)) result = await tools.execute("resp_tool", {}, my_tool) - subscribers.flush() + await subscribers.flush_async() # Sanitize guardrails are observability-only: they do NOT modify the # result flowing through the execution pipeline. @@ -132,7 +132,7 @@ def my_tool(args): scope_local.register_tool_sanitize_request(handle, "sl_cleanup_guard", 1, sanitizer) scope_local.register_subscriber(handle, "sl_cleanup_sub", lambda e: events_inside.append(e)) await tools.execute("tool_inside", {"x": 1}, my_tool) - subscribers.flush() + await subscribers.flush_async() # Verify the sanitizer ran inside the scope (visible in event input). start_inside = _scope_event(events_inside, "tool_inside", "tool", "start") @@ -144,7 +144,7 @@ def my_tool(args): subscribers.register("sl_cleanup_outer_sub", lambda e: events_outside.append(e)) await tools.execute("tool_outside", {"x": 2}, my_tool) try: - subscribers.flush() + await subscribers.flush_async() finally: subscribers.deregister("sl_cleanup_outer_sub") @@ -185,7 +185,7 @@ async def test_subscriber_inactive_after_scope_exit(self): subscribers.register("sl_after_sub", lambda e: global_events.append(e)) outer_handle = tools.call("outer_tool", {}) tools.call_end(outer_handle, {}) - subscribers.flush() + await subscribers.flush_async() subscribers.deregister("sl_after_sub") assert len(events_inside) >= 1 @@ -275,7 +275,7 @@ async def test_subscriber_receives_events_in_scope(self): scope_local.register_subscriber(handle, "sl_sub", lambda e: events.append(e)) tool_handle = tools.call("sub_test_tool", {"arg": "value"}) tools.call_end(tool_handle, {"result": "ok"}) - subscribers.flush() + await subscribers.flush_async() # Should have received at least tool start and end events assert len(events) >= 2 @@ -290,7 +290,7 @@ async def test_subscriber_receives_mark_events(self): with scope.scope("mark_sub_scope", ScopeType.Agent) as handle: scope_local.register_subscriber(handle, "sl_mark_sub", lambda e: events.append(e)) scope.event("test_mark", data={"info": "hello"}) - subscribers.flush() + await subscribers.flush_async() mark_events = [e for e in events if isinstance(e, MarkEvent)] assert len(mark_events) >= 1 @@ -436,13 +436,13 @@ def my_tool(args): scope_local.register_tool_request(handle_a, "sl_iso_int", 1, False, intercept_fn) scope_local.register_subscriber(handle_a, "sl_iso_sub_a", lambda e: events_a.append(e)) result_a = await tools.execute("iso_tool_a", {"val": 1}, my_tool) - subscribers.flush() + await subscribers.flush_async() # Scope B: only a subscriber, no intercept with scope.scope("iso_scope_b", ScopeType.Agent) as handle_b: scope_local.register_subscriber(handle_b, "sl_iso_sub_b", lambda e: events_b.append(e)) result_b = await tools.execute("iso_tool_b", {"val": 2}, my_tool) - subscribers.flush() + await subscribers.flush_async() # Scope A should have had the intercept applied assert result_a["intercepted"] is True @@ -550,7 +550,7 @@ def my_tool(args): scope_local.register_tool_sanitize_request(handle, "sl_dereg_guard", 1, sanitizer) scope_local.register_subscriber(handle, "sl_dereg_sub", lambda e: events.append(e)) await tools.execute("dereg_tool_1", {"a": 1}, my_tool) - subscribers.flush() + await subscribers.flush_async() # Verify the sanitizer ran (visible in event input). start_before = _scope_event(events, "dereg_tool_1", "tool", "start") @@ -562,7 +562,7 @@ def my_tool(args): events.clear() await tools.execute("dereg_tool_2", {"a": 2}, my_tool) - subscribers.flush() + await subscribers.flush_async() # After deregistration, the sanitizer should no longer appear in events. start_after = _scope_event(events, "dereg_tool_2", "tool", "start") @@ -667,7 +667,7 @@ def sanitize_request(req, context): scope_local.register_subscriber(handle, "sl_llm_sanitize_sub", lambda event: events.append(event)) scope_local.register_llm_sanitize_request(handle, "sl_llm_sanitize", 1, sanitize_request) result = await llm.execute("sl_llm_sanitize_call", request, lambda req: {"model": req.content["model"]}) - subscribers.flush() + await subscribers.flush_async() assert result == {"model": "scope-local"} start = _scope_event(events, "sl_llm_sanitize_call", "llm", "start") diff --git a/python/tests/test_tools.py b/python/tests/test_tools.py index f6d74c084..37acf0434 100644 --- a/python/tests/test_tools.py +++ b/python/tests/test_tools.py @@ -209,7 +209,7 @@ def failing(args): await tools.execute("failing_tool", {"x": 1}, failing) try: - subscribers.flush() + await subscribers.flush_async() finally: subscribers.deregister("py_tool_exec_failure_sub") @@ -316,6 +316,22 @@ def test_deregister_nonexistent(self): class TestToolGuardrailsAsync: + async def test_async_conditional_runs_on_originating_loop(self): + originating_loop = asyncio.get_running_loop() + + async def allow(_name, _args): + await asyncio.sleep(0) + assert asyncio.get_running_loop() is originating_loop + return None + + guardrails.register_tool_conditional_execution("py_async_conditional_loop", 1, allow) + try: + result = await tools.execute("allowed_tool", {}, lambda args: args) + finally: + guardrails.deregister_tool_conditional_execution("py_async_conditional_loop") + + assert result == {} + async def test_manual_async_sanitizers_publish_transformed_payloads_and_can_flush(self): events = [] request_flushed = False @@ -341,7 +357,7 @@ async def sanitize_response(name, response): try: handle = tools.call("py_manual_tool_flush", {"original": True}) tools.call_end(handle, {"ok": True}) - await asyncio.wait_for(asyncio.to_thread(subscribers.flush), timeout=2) + await asyncio.wait_for(subscribers.flush_async(), timeout=2) finally: guardrails.deregister_tool_sanitize_request("py_manual_tool_flush_request") guardrails.deregister_tool_sanitize_response("py_manual_tool_flush_response") @@ -424,6 +440,22 @@ def test_request_intercept_raises_on_unserializable_return(self): class TestToolInterceptsAsync: + async def test_async_request_intercept_runs_on_originating_loop(self): + originating_loop = asyncio.get_running_loop() + + async def intercept_fn(_name, args): + await asyncio.sleep(0) + assert asyncio.get_running_loop() is originating_loop + return {**args, "intercepted": True} + + intercepts.register_tool_request("py_async_request_loop", 1, False, intercept_fn) + try: + result = await tools.execute("intercepted_tool", {}, lambda args: args) + finally: + intercepts.deregister_tool_request("py_async_request_loop") + + assert result == {"intercepted": True} + async def test_request_intercept_modifies_args(self): def intercept_fn(name, args): args["intercepted"] = True @@ -473,7 +505,7 @@ def original(args): try: result = await tools.execute("next_tool", {"value": 2}, original) assert result == {"value": 6, "from_intercept": True} - subscribers.flush() + await subscribers.flush_async() start = _tool_event(events, "next_tool", "start") end = _tool_event(events, "next_tool", "end") mark = next( From f5a38181d1bd8da24b99b9677fc88cf2d1f47358 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 01:17:51 -0400 Subject: [PATCH 42/83] fix: close async middleware lifecycle gaps Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 310 ++++++++++++++---- crates/core/src/plugin/dynamic/native.rs | 235 +++++++------ .../tests/fixtures/native_plugin/src/lib.rs | 26 +- .../tests/integration/native_plugin_tests.rs | 40 +++ crates/core/tests/unit/native_plugin_tests.rs | 126 ++++++- crates/node/src/api/mod.rs | 34 +- crates/node/src/callable.rs | 46 +-- crates/node/src/callback_factory.rs | 27 +- crates/node/src/promise_call.rs | 7 - crates/node/tests/event_sanitizers_tests.mjs | 41 +++ crates/node/tests/llm_tests.mjs | 135 ++++++++ crates/plugin/src/lib.rs | 31 +- crates/python/src/py_api/mod.rs | 229 ++++++++----- crates/python/src/py_callable.rs | 45 ++- .../tests/coverage/py_api_coverage_tests.rs | 11 +- docs/about-nemo-relay/concepts/middleware.mdx | 21 +- .../dynamic-plugins/native-dynamic/about.mdx | 24 +- python/nemo_relay/_native.pyi | 12 + python/nemo_relay/llm.py | 14 +- python/nemo_relay/subscribers.py | 54 ++- python/nemo_relay/tools.py | 12 +- python/tests/test_event_sanitizers.py | 27 ++ python/tests/test_subscribers.py | 78 +++++ 23 files changed, 1176 insertions(+), 409 deletions(-) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index fc77a9bcc..5e8412944 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -9,8 +9,68 @@ use crate::api::runtime::{ EventSanitizeFn, EventSubscriberFn, NemoRelayContextState, ScopeStackHandle, }; use crate::error::Result; +use std::any::Any; +use std::cell::RefCell; use std::future::Future; use std::pin::Pin; +use std::sync::Arc; + +/// Binding-owned context captured when an event is emitted. +/// +/// The dispatcher treats this value as opaque. Bindings can use it to carry +/// task-local state from synchronous emission into queued middleware. +pub type PublicationContext = Arc; + +thread_local! { + static THREAD_PUBLICATION_CONTEXT: RefCell> = const { RefCell::new(None) }; +} +tokio::task_local! { + static TASK_PUBLICATION_CONTEXT: Option; +} + +struct ThreadPublicationContextGuard(Option); + +impl Drop for ThreadPublicationContextGuard { + fn drop(&mut self) { + THREAD_PUBLICATION_CONTEXT.with(|current| { + current.replace(self.0.take()); + }); + } +} + +fn current_publication_context() -> Option { + TASK_PUBLICATION_CONTEXT + .try_with(Clone::clone) + .ok() + .flatten() + .or_else(|| THREAD_PUBLICATION_CONTEXT.with(|current| current.borrow().clone())) +} + +/// Run synchronous event emission with an opaque binding context snapshot. +#[doc(hidden)] +pub fn with_publication_context( + context: Option, + f: impl FnOnce() -> T, +) -> T { + let previous = THREAD_PUBLICATION_CONTEXT.with(|current| current.replace(context)); + let _guard = ThreadPublicationContextGuard(previous); + f() +} + +/// Run asynchronous event emission with an opaque binding context snapshot. +#[doc(hidden)] +pub async fn with_task_publication_context( + context: Option, + future: F, +) -> F::Output { + TASK_PUBLICATION_CONTEXT.scope(context, future).await +} + +/// Return a typed binding context while queued middleware is running. +#[doc(hidden)] +pub fn publication_context() -> Option> { + current_publication_context()?.downcast().ok() +} pub(crate) type EventTransformFn = Box< dyn FnOnce(Event) -> Pin + Send + 'static>> + Send + 'static, @@ -18,11 +78,10 @@ pub(crate) type EventTransformFn = Box< mod native { use std::cell::{Cell, RefCell}; - use std::collections::VecDeque; use std::panic::{AssertUnwindSafe, catch_unwind}; - use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; + use std::sync::{LazyLock, Mutex, MutexGuard}; use super::*; use crate::api::runtime::scope_stack::{ @@ -38,6 +97,7 @@ mod native { sanitizers: Vec>, subscribers: Vec, scope_stack: ScopeStackHandle, + publication_context: Option, }, Flush { done: Sender<()>, @@ -47,14 +107,17 @@ mod native { }, } - static DISPATCHER: OnceLock, String>> = - OnceLock::new(); - static SANITIZER_RUNTIME: OnceLock> = - OnceLock::new(); + type DispatcherState = Option, String>>; + type SanitizerRuntimeState = Option>; + + static DISPATCHER: LazyLock> = LazyLock::new(|| Mutex::new(None)); + static SANITIZER_RUNTIME: LazyLock> = + LazyLock::new(|| Mutex::new(None)); static DISPATCHER_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); static SANITIZER_RUNTIME_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); thread_local! { static IN_DISPATCHER: Cell = const { Cell::new(false) }; + static FORK_GUARDS: RefCell> = const { RefCell::new(None) }; } tokio::task_local! { static ASYNC_PUBLICATION_MESSAGES: RefCell>>; @@ -62,6 +125,11 @@ mod native { struct DispatchGuard; + struct ForkGuards { + sanitizer_runtime: MutexGuard<'static, SanitizerRuntimeState>, + dispatcher: MutexGuard<'static, DispatcherState>, + } + pub(crate) struct AsyncPublication { sender: Sender>, } @@ -79,23 +147,25 @@ mod native { } } - fn sanitizer_runtime() -> std::result::Result<&'static tokio::runtime::Runtime, String> { - SANITIZER_RUNTIME - .get_or_init(|| { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| error.to_string()) - }) - .as_ref() - .map_err(Clone::clone) - } - #[cfg(test)] pub(super) fn block_on_sanitizer_future( future: F, ) -> std::result::Result { - sanitizer_runtime().map(|runtime| runtime.block_on(future)) + let mut runtime = SANITIZER_RUNTIME + .lock() + .unwrap_or_else(|error| error.into_inner()); + let runtime = runtime.get_or_insert_with(build_sanitizer_runtime); + runtime + .as_ref() + .map(|runtime| runtime.block_on(future)) + .map_err(Clone::clone) + } + + fn build_sanitizer_runtime() -> std::result::Result { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| error.to_string()) } pub(super) fn dispatch_event(event: &Event, subscribers: &[EventSubscriberFn]) -> bool { @@ -108,6 +178,7 @@ mod native { sanitizers: Vec::new(), subscribers: subscribers.to_vec(), scope_stack: current_scope_stack(), + publication_context: current_publication_context(), }; send_dispatch_message(message) } @@ -127,6 +198,7 @@ mod native { sanitizers, subscribers: subscribers.to_vec(), scope_stack, + publication_context: current_publication_context(), }; send_dispatch_message(message) } @@ -146,6 +218,7 @@ mod native { sanitizers, subscribers: subscribers.to_vec(), scope_stack, + publication_context: current_publication_context(), }; let buffer_active = ASYNC_PUBLICATION_MESSAGES .try_with(|messages| messages.borrow().is_some()) @@ -177,6 +250,7 @@ mod native { sanitizers, subscribers: subscribers.to_vec(), scope_stack, + publication_context: current_publication_context(), }; send_dispatch_message(message) } @@ -201,12 +275,16 @@ mod native { if in_dispatcher_callback() { return Ok(()); } - let Some(sender_result) = DISPATCHER.get() else { - return Ok(()); + let sender = { + let dispatcher = DISPATCHER.lock().unwrap_or_else(|error| error.into_inner()); + let Some(sender_result) = dispatcher.as_ref() else { + return Ok(()); + }; + sender_result + .as_ref() + .map_err(|error| FlowError::Internal(error.clone()))? + .clone() }; - let sender = sender_result - .as_ref() - .map_err(|error| FlowError::Internal(error.clone()))?; let (done_tx, done_rx) = mpsc::channel(); sender .send(DispatcherMessage::Flush { done: done_tx }) @@ -249,7 +327,8 @@ mod native { } fn dispatcher_sender() -> std::result::Result, String> { - DISPATCHER.get_or_init(start_dispatcher).clone() + let mut dispatcher = DISPATCHER.lock().unwrap_or_else(|error| error.into_inner()); + dispatcher.get_or_insert_with(start_dispatcher).clone() } fn send_dispatch_message(message: DispatcherMessage) -> bool { @@ -294,22 +373,10 @@ mod native { } fn run_dispatcher(rx: Receiver) { - let mut pending = VecDeque::new(); - loop { - let message = match pending.pop_front() { - Some(message) => message, - None => match rx.recv() { - Ok(message) => message, - Err(_) => break, - }, - }; + while let Ok(message) = rx.recv() { match message { DispatcherMessage::Flush { done } => { - let pending_flushes = drain_pending_messages(&rx, &mut pending); let _ = done.send(()); - for pending in pending_flushes { - let _ = pending.send(()); - } } DispatcherMessage::Barrier { publications } => { if let Ok(publications) = publications.recv() { @@ -323,24 +390,6 @@ mod native { } } - fn drain_pending_messages( - rx: &Receiver, - pending: &mut VecDeque, - ) -> Vec> { - let mut pending_flushes = Vec::new(); - while let Ok(message) = rx.try_recv() { - match message { - DispatcherMessage::Flush { done } => pending_flushes.push(done), - message @ DispatcherMessage::Barrier { .. } => { - pending.push_back(message); - break; - } - message => handle_message(message), - } - } - pending_flushes - } - fn handle_message(message: DispatcherMessage) { match message { DispatcherMessage::Deliver { @@ -349,7 +398,15 @@ mod native { sanitizers, subscribers, scope_stack, - } => deliver_event(event, transform, sanitizers, subscribers, scope_stack), + publication_context, + } => deliver_event( + event, + transform, + sanitizers, + subscribers, + scope_stack, + publication_context, + ), DispatcherMessage::Flush { done } => { let _ = done.send(()); } @@ -369,11 +426,14 @@ mod native { sanitizers: Vec>, subscribers: Vec, scope_stack: ScopeStackHandle, + publication_context: Option, ) { let previous_scope_stack = capture_thread_scope_stack(); set_thread_scope_stack(scope_stack); let _dispatch_guard = DispatchGuard::enter(); - let Some(event) = sanitize_event_snapshot(*event, transform, sanitizers) else { + let Some(event) = + sanitize_event_snapshot(*event, transform, sanitizers, publication_context) + else { restore_thread_scope_stack(previous_scope_stack); return; }; @@ -397,8 +457,13 @@ mod native { event: Event, transform: Option, sanitizers: Vec>, + publication_context: Option, ) -> Option { - let runtime = match sanitizer_runtime() { + let mut runtime = SANITIZER_RUNTIME + .lock() + .unwrap_or_else(|error| error.into_inner()); + let runtime = runtime.get_or_insert_with(build_sanitizer_runtime); + let runtime = match runtime.as_ref() { Ok(runtime) => runtime, Err(error) => { if !SANITIZER_RUNTIME_FAILURE_LOGGED.swap(true, Ordering::AcqRel) { @@ -411,13 +476,16 @@ mod native { return None; } }; + let transform_context = publication_context.clone(); let transformed = match catch_unwind(AssertUnwindSafe(|| { - runtime.block_on(async move { - match transform { - Some(transform) => transform(event).await, - None => event, - } - }) + runtime.block_on( + TASK_PUBLICATION_CONTEXT.scope(transform_context, async move { + match transform { + Some(transform) => transform(event).await, + None => event, + } + }), + ) })) { Ok(event) => event, Err(_) => { @@ -434,9 +502,9 @@ mod native { } let fallback = transformed.clone(); match catch_unwind(AssertUnwindSafe(|| { - runtime.block_on(NemoRelayContextState::event_sanitize_snapshot_chain( - transformed, - &sanitizers, + runtime.block_on(TASK_PUBLICATION_CONTEXT.scope( + publication_context, + NemoRelayContextState::event_sanitize_snapshot_chain(transformed, &sanitizers), )) })) { Ok(event) => Some(event), @@ -451,6 +519,44 @@ mod native { } } + pub(super) fn prepare_for_fork() { + // Lock in the same order used by publication: sanitizer execution can + // enqueue another event and therefore acquire the dispatcher lock. + let sanitizer_runtime = SANITIZER_RUNTIME + .lock() + .unwrap_or_else(|error| error.into_inner()); + let dispatcher = DISPATCHER.lock().unwrap_or_else(|error| error.into_inner()); + FORK_GUARDS.with(|guards| { + let previous = guards.replace(Some(ForkGuards { + sanitizer_runtime, + dispatcher, + })); + assert!(previous.is_none(), "subscriber fork preparation is nested"); + }); + } + + pub(super) fn resume_after_fork_parent() { + FORK_GUARDS.with(|guards| { + guards + .borrow_mut() + .take() + .expect("subscriber fork parent hook ran without preparation"); + }); + } + + pub(super) fn reset_after_fork_child() { + FORK_GUARDS.with(|guards| { + let mut guards = guards + .borrow_mut() + .take() + .expect("subscriber fork child hook ran without preparation"); + *guards.dispatcher = None; + *guards.sanitizer_runtime = None; + }); + DISPATCHER_FAILURE_LOGGED.store(false, Ordering::Release); + SANITIZER_RUNTIME_FAILURE_LOGGED.store(false, Ordering::Release); + } + #[cfg(test)] mod tests { use super::*; @@ -488,6 +594,7 @@ mod native { sanitizers: Vec::new(), subscribers: vec![subscriber.clone()], scope_stack: current_scope_stack(), + publication_context: None, }) .unwrap(); let (flush_tx, flush_rx) = mpsc::channel(); @@ -518,6 +625,7 @@ mod native { sanitizers: Vec::new(), subscribers: vec![subscriber], scope_stack: current_scope_stack(), + publication_context: None, }]) .unwrap(); flush_rx @@ -531,6 +639,54 @@ mod native { later.sender.send(Vec::new()).unwrap(); flush_subscribers().unwrap(); } + + #[test] + fn flush_does_not_wait_for_later_delivery() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + flush_subscribers().unwrap(); + let barrier = register_async_publication().expect("publication barrier"); + let sender = dispatcher_sender().expect("dispatcher sender"); + let (flush_tx, flush_rx) = mpsc::channel(); + sender + .send(DispatcherMessage::Flush { done: flush_tx }) + .unwrap(); + + let (release_tx, release_rx) = tokio::sync::oneshot::channel(); + let event = serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000003", + "timestamp": "2026-07-28T00:00:00Z", + "name": "queued-after-flush" + })) + .expect("valid event"); + sender + .send(DispatcherMessage::Deliver { + event: Box::new(event), + transform: Some(Box::new(move |event| { + Box::pin(async move { + let _ = release_rx.await; + event + }) + })), + sanitizers: Vec::new(), + subscribers: Vec::new(), + scope_stack: current_scope_stack(), + publication_context: None, + }) + .unwrap(); + barrier.sender.send(Vec::new()).unwrap(); + + let flush_result = flush_rx.recv_timeout(std::time::Duration::from_millis(100)); + let _ = release_tx.send(()); + flush_subscribers().unwrap(); + assert!( + flush_result.is_ok(), + "a delivery queued after a flush must not delay that flush" + ); + } } } @@ -605,6 +761,24 @@ pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() } +/// Acquire process-local dispatcher resources before a Unix `fork`. +#[doc(hidden)] +pub fn prepare_for_fork() { + native::prepare_for_fork(); +} + +/// Release process-local dispatcher resources in the parent after a Unix `fork`. +#[doc(hidden)] +pub fn resume_after_fork_parent() { + native::resume_after_fork_parent(); +} + +/// Reset and release inherited dispatcher resources in the child after a Unix `fork`. +#[doc(hidden)] +pub fn reset_after_fork_child() { + native::reset_after_fork_child(); +} + /// Return whether the current callback was invoked by queued event publication. /// /// Bindings use this to make re-entrant flush operations non-blocking while diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 42ba1684b..095db85ad 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -1381,41 +1381,12 @@ fn make_user_data( }) } -/// One-shot state retained by a v3 native async callback. -enum NativeAsyncResult { - Json(Json), - LlmStream(LlmJsonStream), -} - -impl std::fmt::Debug for NativeAsyncResult { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Json(value) => formatter.debug_tuple("Json").field(value).finish(), - Self::LlmStream(_) => formatter.write_str("LlmStream(..)"), - } - } -} - -impl PartialEq for NativeAsyncResult { - fn eq(&self, other: &Json) -> bool { - matches!(self, Self::Json(value) if value == other) - } -} - -impl NativeAsyncResult { - fn into_json(self) -> FlowResult { - match self { - Self::Json(value) => Ok(value), - Self::LlmStream(_) => Err(FlowError::Internal( - "native async callback returned a stream for a non-stream invocation".into(), - )), - } - } -} +const NATIVE_ASYNC_STREAM_CHANNEL_CAPACITY: usize = 64; struct NativeAsyncCompletion { - sender: Mutex>>>, + sender: Mutex>>>, cancelled: AtomicBool, + next_abort: Mutex>, // A pending native callback can continue running after its completion // wakes the awaiting task. Keep the callback's dynamic-library instance // alive until native code explicitly releases this handle. @@ -1424,12 +1395,21 @@ struct NativeAsyncCompletion { struct NativeAsyncWait { completion: Arc, - receiver: tokio::sync::oneshot::Receiver>, + receiver: tokio::sync::oneshot::Receiver>, } impl Drop for NativeAsyncWait { fn drop(&mut self) { self.completion.cancelled.store(true, Ordering::Release); + if let Some(abort) = self + .completion + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + abort.abort(); + } } } @@ -1449,14 +1429,14 @@ struct NativeAsyncNext { } struct NativeAsyncStream { - sender: Mutex>>>, + sender: Mutex>>>, cancelled: AtomicBool, downstream_abort: Mutex>, _callback_user_data: Option>, } struct NativeAsyncStreamReceiver { - receiver: tokio::sync::mpsc::UnboundedReceiver>, + receiver: tokio::sync::mpsc::Receiver>, stream: Arc, } @@ -1520,7 +1500,7 @@ async fn invoke_native_async_callback( user_data: Arc, invocation: Json, next: Option, -) -> FlowResult { +) -> FlowResult { let runtime = if next.is_some() { Some(tokio::runtime::Handle::try_current().map_err(|error| { FlowError::Internal(format!( @@ -1537,6 +1517,7 @@ async fn invoke_native_async_callback( let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_abort: Mutex::new(None), _callback_user_data: Some(user_data.clone()), }); let completion_ref = Arc::into_raw(completion.clone()) as usize; @@ -1626,6 +1607,14 @@ unsafe extern "C" fn native_async_completion_resolve_json( Ok(value) => value, Err(status) => return status, }; + if let Some(abort) = completion + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + abort.abort(); + } let Some(sender) = completion .sender .lock() @@ -1634,7 +1623,7 @@ unsafe extern "C" fn native_async_completion_resolve_json( else { return NemoRelayStatus::InvalidArg; }; - let _ = sender.send(Ok(NativeAsyncResult::Json(value))); + let _ = sender.send(Ok(value)); NemoRelayStatus::Ok } @@ -1660,6 +1649,14 @@ unsafe extern "C" fn native_async_completion_reject( } } }; + if let Some(abort) = completion + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + abort.abort(); + } let Some(sender) = completion .sender .lock() @@ -1697,6 +1694,7 @@ unsafe extern "C" fn native_async_stream_push_json( stream: *const NemoRelayNativeAsyncStream, chunk_json: *const NemoRelayNativeString, ) -> NemoRelayStatus { + clear_native_last_error(); let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else { return NemoRelayStatus::NullPointer; }; @@ -1712,9 +1710,18 @@ unsafe extern "C" fn native_async_stream_push_json( .lock() .unwrap_or_else(|error| error.into_inner()) .clone(); - match sender { - Some(sender) if sender.send(Ok(chunk)).is_ok() => NemoRelayStatus::Ok, - _ => NemoRelayStatus::InvalidArg, + let Some(sender) = sender else { + return NemoRelayStatus::InvalidArg; + }; + match sender.try_send(Ok(chunk)) { + Ok(()) => NemoRelayStatus::Ok, + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + set_native_last_error( + "native async stream is backpressured; retry the chunk after the consumer advances", + ); + NemoRelayStatus::Internal + } + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => NemoRelayStatus::InvalidArg, } } @@ -1741,22 +1748,31 @@ unsafe extern "C" fn native_async_stream_reject( stream: *const NemoRelayNativeAsyncStream, message: *const NemoRelayNativeString, ) -> NemoRelayStatus { + clear_native_last_error(); let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else { return NemoRelayStatus::NullPointer; }; let message = read_native_string(message).unwrap_or_else(|_| "native async stream rejected".to_string()); - let sender = stream + let mut sender_guard = stream .sender .lock() - .unwrap_or_else(|error| error.into_inner()) - .take(); - match sender { - Some(sender) => { - let _ = sender.send(Err(FlowError::Internal(message))); + .unwrap_or_else(|error| error.into_inner()); + let Some(sender) = sender_guard.as_ref() else { + return NemoRelayStatus::InvalidArg; + }; + match sender.try_send(Err(FlowError::Internal(message))) { + Ok(()) => { + sender_guard.take(); NemoRelayStatus::Ok } - None => NemoRelayStatus::InvalidArg, + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + set_native_last_error( + "native async stream is backpressured; retry rejection after the consumer advances", + ); + NemoRelayStatus::Internal + } + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => NemoRelayStatus::InvalidArg, } } @@ -1789,16 +1805,22 @@ unsafe extern "C" fn native_async_next_invoke( Ok(value) => value, Err(status) => return status, }; + if matches!(&next.inner, NativeAsyncNextInner::LlmStream(_)) { + set_native_last_error( + "stream continuations require async_next_invoke_stream; completion-based next cannot buffer a stream", + ); + return NemoRelayStatus::InvalidArg; + } unsafe { Arc::increment_strong_count(completion as *const NativeAsyncCompletion) }; let completion = unsafe { Arc::from_raw(completion as *const NativeAsyncCompletion) }; - let future: Pin> + Send>> = match &next - .inner - { + if completion.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } + let future: Pin> + Send>> = match &next.inner { NativeAsyncNextInner::Tool(next) => { let next = next.clone(); Box::pin(async move { serde_json::to_value(ToolExecutionInterceptOutcome::new(next(invocation).await?)) - .map(NativeAsyncResult::Json) .map_err(|error| { FlowError::Internal(format!( "failed to serialize native async tool outcome: {error}" @@ -1820,30 +1842,30 @@ unsafe extern "C" fn native_async_next_invoke( } }; let next = next.clone(); - Box::pin(async move { next(request).await.map(NativeAsyncResult::Json) }) - } - NativeAsyncNextInner::LlmStream(next) => { - let request = match serde_json::from_value(invocation) { - Ok(request) => request, - Err(error) => { - let _ = completion - .sender - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - .map(|sender| sender.send(Err(FlowError::Internal(error.to_string())))); - return NemoRelayStatus::InvalidArg; - } - }; - let next = next.clone(); - Box::pin(async move { next(request).await.map(NativeAsyncResult::LlmStream) }) + Box::pin(async move { next(request).await }) } + NativeAsyncNextInner::LlmStream(_) => unreachable!("stream continuations were rejected"), }; let scope_stack = next.scope_stack.clone(); - next.runtime + let mut abort_guard = completion + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()); + if abort_guard.is_some() { + set_native_last_error("native async next was already invoked for this completion"); + return NemoRelayStatus::InvalidArg; + } + let completion_for_task = Arc::clone(&completion); + let task = next + .runtime .spawn(TASK_SCOPE_STACK.scope(scope_stack, async move { let result = future.await; - if let Some(sender) = completion + completion_for_task + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take(); + if let Some(sender) = completion_for_task .sender .lock() .unwrap_or_else(|error| error.into_inner()) @@ -1852,6 +1874,7 @@ unsafe extern "C" fn native_async_next_invoke( let _ = sender.send(result); } })); + *abort_guard = Some(task.abort_handle()); NemoRelayStatus::Ok } @@ -1980,8 +2003,7 @@ fn wrap_native_async_tool_json( serde_json::json!({"name": name, "value": value}), None, ) - .await? - .into_json()?; + .await?; Ok(value) }) }) @@ -2004,7 +2026,6 @@ fn wrap_native_async_tool_conditional( None, ) .await? - .into_json()? { Json::Null => Ok(None), Json::String(reason) => Ok(Some(reason)), @@ -2033,7 +2054,6 @@ fn wrap_native_async_llm_conditional( None, ) .await? - .into_json()? { Json::Null => Ok(None), Json::String(reason) => Ok(Some(reason)), @@ -2062,8 +2082,7 @@ fn wrap_native_async_llm_sanitize_request( serde_json::json!({"request": request, "context": codec}), None, ) - .await? - .into_json()?; + .await?; if value.is_null() { Ok(None) } else { @@ -2092,8 +2111,7 @@ fn wrap_native_async_llm_sanitize_response( serde_json::json!({"response": response, "context": codec}), None, ) - .await? - .into_json()?; + .await?; Ok((!value.is_null()).then_some(value)) }) }) @@ -2137,8 +2155,7 @@ fn wrap_native_async_llm_request_intercept( }), None, ) - .await? - .into_json()?, + .await?, ) .map_err(|error| { FlowError::Internal(format!( @@ -2166,8 +2183,7 @@ fn wrap_native_async_event_sanitize( serde_json::json!({"event": event, "fields": fields}), None, ) - .await? - .into_json()?, + .await?, ) .map_err(|error| { FlowError::Internal(format!("invalid native async event fields: {error}")) @@ -2194,8 +2210,7 @@ fn wrap_native_async_tool_execution( invocation, Some(NativeAsyncNextInner::Tool(next)), ) - .await? - .into_json()?, + .await?, ) .map_err(|error| { FlowError::Internal(format!("invalid native async tool outcome: {error}")) @@ -2221,39 +2236,7 @@ fn wrap_native_async_llm_execution( serde_json::json!({"name": name, "request": request}), Some(NativeAsyncNextInner::Llm(next)), ) - .await? - .into_json() - }) - }) -} - -fn wrap_native_async_llm_stream_execution( - instance: Arc, - cb: NemoRelayNativeAsyncMiddlewareCb, - user_data: *mut c_void, - free_fn: NemoRelayNativeFreeFn, -) -> LlmStreamExecutionFn { - let user_data = make_user_data(instance, user_data, free_fn); - Arc::new(move |name, request, next| { - let user_data = user_data.clone(); - let name = name.to_owned(); - Box::pin(async move { - let value = invoke_native_async_callback( - cb, - user_data, - serde_json::json!({"name": name, "request": request}), - Some(NativeAsyncNextInner::LlmStream(next)), - ) - .await?; - match value { - NativeAsyncResult::LlmStream(stream) => Ok(stream), - NativeAsyncResult::Json(Json::Array(chunks)) => Ok(LlmJsonStream::new( - tokio_stream::iter(chunks.into_iter().map(Ok)), - )), - NativeAsyncResult::Json(_) => Err(FlowError::Internal( - "native async LLM stream intercept must resolve to an array".into(), - )), - } + .await }) }) } @@ -2287,7 +2270,8 @@ fn wrap_native_incremental_llm_stream_execution( scope_stack: current_scope_stack(), _callback_user_data: Some(user_data.clone()), })); - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let (sender, receiver) = + tokio::sync::mpsc::channel(NATIVE_ASYNC_STREAM_CHANNEL_CAPACITY); let stream = Arc::new(NativeAsyncStream { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), @@ -2400,6 +2384,12 @@ unsafe extern "C" fn native_plugin_context_register_async_middleware( return NemoRelayStatus::InvalidArg; } }; + if kind == NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept { + set_native_last_error( + "completion-based LLM stream middleware is unsupported; use plugin_context_register_async_stream_middleware", + ); + return NemoRelayStatus::InvalidArg; + } if kind == NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept && let Err(error) = validate_annotated_request_consumer_compatibility( &instance.relay_compat, @@ -2473,12 +2463,9 @@ unsafe extern "C" fn native_plugin_context_register_async_middleware( priority, wrap_native_async_llm_execution(instance, cb, user_data, free_fn), ), - NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept => context - .register_llm_stream_execution_intercept( - &name, - priority, - wrap_native_async_llm_stream_execution(instance, cb, user_data, free_fn), - ), + NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept => { + unreachable!("completion-based stream middleware was rejected before registration") + } NemoRelayNativeAsyncMiddlewareKind::MarkSanitize => context .register_mark_sanitize_guardrail( &name, diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index 753948ee4..da5752fb7 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -1005,15 +1005,22 @@ unsafe extern "C" fn raw_async_tool_execution_callback( let value = unsafe { raw_host_string_value(&host.v1, invocation_json) } .and_then(|json| serde_json::from_str::(&json).ok()) .and_then(|mut invocation| { - if let Some(value) = invocation.get_mut("value").and_then(Json::as_object_mut) { - value.insert("native_async_execution".into(), json!(true)); - Some(Json::Object(value.clone())) - } else { - invocation.get("request").cloned() - } + let cancellation_probe = invocation["name"].as_str() == Some("async-cancel-next"); + let value = + if let Some(value) = invocation.get_mut("value").and_then(Json::as_object_mut) { + value.insert("native_async_execution".into(), json!(true)); + Some(Json::Object(value.clone())) + } else { + invocation.get("request").cloned() + }; + value.map(|value| (value, cancellation_probe)) }) - .and_then(|value| serde_json::to_string(&value).ok()); - let Some(value) = value else { + .and_then(|(value, cancellation_probe)| { + serde_json::to_string(&value) + .ok() + .map(|value| (value, cancellation_probe)) + }); + let Some((value, cancellation_probe)) = value else { unsafe { reject_async_completion(host, completion, "invalid async tool execution invocation") }; @@ -1037,6 +1044,9 @@ unsafe extern "C" fn raw_async_tool_execution_callback( (host.v1.string_free)(value); } if status == NemoRelayStatus::Ok { + if cancellation_probe { + ASYNC_PENDING_ENTERED.store(true, Ordering::Release); + } unsafe { (host.async_next_release)(next); (host.async_completion_release)(completion); diff --git a/crates/core/tests/integration/native_plugin_tests.rs b/crates/core/tests/integration/native_plugin_tests.rs index 8e9782def..82fc2168e 100644 --- a/crates/core/tests/integration/native_plugin_tests.rs +++ b/crates/core/tests/integration/native_plugin_tests.rs @@ -792,6 +792,46 @@ async fn native_v3_async_registration_supports_all_middleware_kinds() { .expect("pending v3 async request intercept should settle after clear"); assert_eq!(pending["native_async"], true); + let mut config = PluginConfig::default(); + config.components.push(PluginComponentSpec { + kind: "fixture_async".into(), + enabled: true, + config: Map::new(), + }); + initialize_plugins_exact(config) + .await + .expect("v3 async native fixture should reactivate"); + cleanup.mark_plugin_configuration_active(); + let pending_next = tokio::spawn(async { + tool_call_execute( + ToolCallExecuteParams::builder() + .name("async-cancel-next") + .args(json!({"input": true})) + .func(Arc::new(|_args| { + Box::pin(async { std::future::pending().await }) + })) + .build(), + ) + .await + }); + tokio::time::timeout(std::time::Duration::from_secs(10), async { + while !unsafe { pending_entered() } { + tokio::task::yield_now().await; + } + }) + .await + .expect("native async next should start before cancellation"); + clear_plugin_configuration().expect("plugin configuration should clear with next pending"); + cleanup.plugin_configuration_active = false; + pending_next.abort(); + assert!( + pending_next + .await + .expect_err("pending next should be cancelled") + .is_cancelled(), + "aborting the managed call should cancel its native next continuation" + ); + drop(cleanup); drop(activation); } diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index 4c3aa9848..c2a256556 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -298,6 +298,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_abort: Mutex::new(None), _callback_user_data: None, }); let completion_ref = @@ -329,10 +330,11 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { _callback_user_data: None, }); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; - let (sender, receiver) = tokio::sync::oneshot::channel(); + let (sender, _receiver) = tokio::sync::oneshot::channel(); let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_abort: Mutex::new(None), _callback_user_data: None, }); let completion_ref = @@ -347,16 +349,9 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { .unwrap(); assert_eq!( unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, - NemoRelayStatus::Ok - ); - let NativeAsyncResult::LlmStream(mut stream) = runtime.block_on(receiver).unwrap().unwrap() - else { - panic!("stream continuation should preserve the downstream stream"); - }; - assert_eq!( - runtime.block_on(stream.next()).unwrap().unwrap(), - json!({"chunk": 1}) + NemoRelayStatus::InvalidArg ); + assert_last_error_contains("async_next_invoke_stream"); unsafe { native_string_free(invocation); native_async_next_release(next_ref); @@ -374,6 +369,7 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_abort: Mutex::new(None), _callback_user_data: None, }); let completion_ref = @@ -406,6 +402,7 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(true), + next_abort: Mutex::new(None), _callback_user_data: None, }); let completion_ref = @@ -420,12 +417,94 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen } #[test] -fn native_async_stream_push_finish_and_consumer_cancellation_are_incremental() { +fn cancelling_completion_aborts_pending_native_next() { + struct DropProbe(Arc); + + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let started = Arc::new(AtomicBool::new(false)); + let dropped = Arc::new(AtomicBool::new(false)); + let next = Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::Llm({ + let started = Arc::clone(&started); + let dropped = Arc::clone(&dropped); + Arc::new(move |_request| { + let started = Arc::clone(&started); + let probe = DropProbe(Arc::clone(&dropped)); + Box::pin(async move { + started.store(true, Ordering::SeqCst); + std::future::pending::<()>().await; + drop(probe); + unreachable!("pending continuation only exits when aborted") + }) + }) + }), + runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_abort: Mutex::new(None), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"pending": true}), + }) + .unwrap(), + ) + .unwrap(); + assert_eq!( + unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, + NemoRelayStatus::Ok + ); + runtime.block_on(tokio::task::yield_now()); + assert!(started.load(Ordering::SeqCst)); + + drop(NativeAsyncWait { + completion: Arc::clone(&completion), + receiver, + }); + runtime.block_on(tokio::task::yield_now()); + assert!(completion.cancelled.load(Ordering::SeqCst)); + assert!(dropped.load(Ordering::SeqCst)); + assert!( + completion + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_none() + ); + + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_completion_release(completion_ref); + } +} + +#[test] +fn native_async_stream_push_is_bounded_retryable_and_incremental() { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let (sender, receiver) = tokio::sync::mpsc::channel(1); let stream = Arc::new(NativeAsyncStream { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), @@ -433,11 +512,17 @@ fn native_async_stream_push_finish_and_consumer_cancellation_are_incremental() { _callback_user_data: None, }); let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; - let chunk = native_string(r#"{"chunk":1}"#); + let first_chunk = native_string(r#"{"chunk":1}"#); + let second_chunk = native_string(r#"{"chunk":2}"#); assert_eq!( - unsafe { native_async_stream_push_json(stream_ref, chunk) }, + unsafe { native_async_stream_push_json(stream_ref, first_chunk) }, NemoRelayStatus::Ok ); + assert_eq!( + unsafe { native_async_stream_push_json(stream_ref, second_chunk) }, + NemoRelayStatus::Internal + ); + assert_last_error_contains("backpressured"); let mut receiver = NativeAsyncStreamReceiver { receiver, stream: Arc::clone(&stream), @@ -446,6 +531,14 @@ fn native_async_stream_push_finish_and_consumer_cancellation_are_incremental() { runtime.block_on(receiver.next()).unwrap().unwrap(), json!({"chunk": 1}) ); + assert_eq!( + unsafe { native_async_stream_push_json(stream_ref, second_chunk) }, + NemoRelayStatus::Ok + ); + assert_eq!( + runtime.block_on(receiver.next()).unwrap().unwrap(), + json!({"chunk": 2}) + ); assert_eq!( unsafe { native_async_stream_finish(stream_ref) }, NemoRelayStatus::Ok @@ -454,11 +547,12 @@ fn native_async_stream_push_finish_and_consumer_cancellation_are_incremental() { drop(receiver); assert!(unsafe { native_async_stream_is_cancelled(stream_ref) }); assert_eq!( - unsafe { native_async_stream_push_json(stream_ref, chunk) }, + unsafe { native_async_stream_push_json(stream_ref, first_chunk) }, NemoRelayStatus::InvalidArg ); unsafe { - native_string_free(chunk); + native_string_free(first_chunk); + native_string_free(second_chunk); native_async_stream_release(stream_ref); } } diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 872420717..7e076832e 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -1342,9 +1342,7 @@ fn node_event_sanitize_fn(env: &Env, func: &JsFunction) -> napi::Result Result<()> { let callback = Arc::new(PromiseAwareFn::new(&env, &guardrail)?); @@ -2749,6 +2752,9 @@ macro_rules! napi_intercept_tool_api { name: String, priority: i32, break_chain: bool, + #[napi( + ts_arg_type = "(toolName: string, args: Json) => Json | Promise" + )] callable: JsFunction, ) -> Result<()> { let callback = std::sync::Arc::new( @@ -2985,6 +2991,9 @@ pub fn register_llm_execution_intercept( env: Env, name: String, priority: i32, + #[napi( + ts_arg_type = "(request: Json, next: (request: Json) => Json | Promise) => Json | Promise" + )] callable: JsFunction, ) -> Result<()> { let pa_fn = std::sync::Arc::new( @@ -3020,6 +3029,9 @@ pub fn register_llm_stream_execution_intercept( env: Env, name: String, priority: i32, + #[napi( + ts_arg_type = "(request: Json, next: (request: Json) => Promise) => Json | Json[] | Promise" + )] callable: JsFunction, ) -> Result<()> { let pa_fn = std::sync::Arc::new( @@ -3175,6 +3187,9 @@ macro_rules! napi_scope_guardrail_tool_api { scope_uuid: String, name: String, priority: i32, + #[napi( + ts_arg_type = "(toolName: string, value: Json) => Json | Promise" + )] guardrail: JsFunction, ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) @@ -3246,6 +3261,9 @@ pub fn scope_register_tool_conditional_execution_guardrail( scope_uuid: String, name: String, priority: i32, + #[napi( + ts_arg_type = "(toolName: string, args: Json) => string | null | Promise" + )] guardrail: JsFunction, ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) @@ -3293,6 +3311,9 @@ macro_rules! napi_scope_intercept_tool_api { name: String, priority: i32, break_chain: bool, + #[napi( + ts_arg_type = "(toolName: string, args: Json) => Json | Promise" + )] callable: JsFunction, ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) @@ -3486,6 +3507,7 @@ pub fn scope_register_llm_conditional_execution_guardrail( scope_uuid: String, name: String, priority: i32, + #[napi(ts_arg_type = "(request: Json) => string | null | Promise")] guardrail: JsFunction, ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) @@ -3577,6 +3599,9 @@ pub fn scope_register_llm_execution_intercept( scope_uuid: String, name: String, priority: i32, + #[napi( + ts_arg_type = "(request: Json, next: (request: Json) => Json | Promise) => Json | Promise" + )] callable: JsFunction, ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) @@ -3618,6 +3643,9 @@ pub fn scope_register_llm_stream_execution_intercept( scope_uuid: String, name: String, priority: i32, + #[napi( + ts_arg_type = "(request: Json, next: (request: Json) => Promise) => Json | Json[] | Promise" + )] callable: JsFunction, ) -> Result<()> { let uuid = uuid::Uuid::parse_str(&scope_uuid) diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index 9d08f1ba0..c3f35be6b 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -479,10 +479,9 @@ pub fn wrap_js_llm_request_intercept_promise_fn( /// Wrap a Promise-aware JS event sanitizer. /// -/// Event sanitizers run on Relay's serial publication dispatcher, not on the -/// JavaScript registration thread. Waiting here therefore preserves synchronous -/// scope/mark APIs while allowing the JavaScript callback to settle a Promise -/// on the Node event loop. +/// Scope and mark publication invokes these callbacks from Relay's serial +/// dispatcher, while managed tool/LLM lifecycle paths can invoke them inline. +/// The invocation context decides whether `flushSubscribers()` is reentrant. pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSanitizeFn { Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); @@ -512,24 +511,29 @@ pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSani })?, metadata: fields.metadata, }; - let value = func - .call_spread(vec![ - event_json, - serde_json::to_value(js_fields).map_err(|error| { - let error = FlowError::Internal(format!( - "failed to serialize JS event sanitizer fields: {error}" - )); - record_callback_error(error.to_string()); - error - })?, - ]) - .await - .inspect_err(|error| { - // Scope and mark publication happens on the dispatcher - // thread. Preserve the event (the core fails open) while - // making the binding-visible failure available to Node. + let args = vec![ + event_json, + serde_json::to_value(js_fields).map_err(|error| { + let error = FlowError::Internal(format!( + "failed to serialize JS event sanitizer fields: {error}" + )); record_callback_error(error.to_string()); - })?; + error + })?, + ]; + let publication = + nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); + let value = if publication { + func.call_spread_for_publication(args).await + } else { + func.call_spread(args).await + } + .inspect_err(|error| { + // Scope and mark publication happens on the dispatcher + // thread. Preserve the event (the core fails open) while + // making the binding-visible failure available to Node. + record_callback_error(error.to_string()); + })?; let fields = event_sanitize_fields_from_json(value).map_err(|error| { let error = FlowError::Internal(format!("invalid JS event sanitizer result: {error}")); diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index 9314c544c..24e250d49 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -77,11 +77,7 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { reject(message); }); }; - if (publication) { - eventSanitizerContext.run(token, invoke); - } else { - invoke(); - } + eventSanitizerContext.run(token, invoke); } return { @@ -114,20 +110,6 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { }; }, - eventSanitizerPromise(fn) { - return function __nemo_relay_event_sanitizer_promise_wrapper(error, arg0, spread, next, resolve, reject) { - if (error != null) { - let message = 'unknown error'; - try { - message = String(error?.message ?? error); - } catch {} - reject(message); - return; - } - callPromise(fn, arg0, spread, next, resolve, reject, true); - }; - }, - eventSanitizerCallbackActive() { return eventSanitizerContext.getStore()?.active === true; }, @@ -178,13 +160,6 @@ pub(crate) fn wrap_promise_callback(env: &Env, func: &JsFunction) -> napi::Resul wrap_callback(env, func, "promise") } -pub(crate) fn wrap_event_sanitizer_callback( - env: &Env, - func: &JsFunction, -) -> napi::Result { - wrap_callback(env, func, "eventSanitizerPromise") -} - pub(crate) fn event_sanitizer_callback_active(env: &Env) -> napi::Result { let factories = callback_factories(env)?; let callback: JsFunction = factories.get_named_property("eventSanitizerCallbackActive")?; diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index ea741558f..9a4249900 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -209,13 +209,6 @@ impl PromiseAwareFn { Self::from_wrapper(env, &wrapper) } - /// Create a callback wrapper that marks only its JavaScript async context - /// as an active event sanitizer. - pub fn new_event_sanitizer(env: &Env, func: &JsFunction) -> napi::Result { - let wrapper = callback_factory::wrap_event_sanitizer_callback(env, func)?; - Self::from_wrapper(env, &wrapper) - } - fn from_wrapper(env: &Env, wrapper: &JsFunction) -> napi::Result { let mut tsfn = env.create_threadsafe_function(wrapper, 0, |ctx: ThreadSafeCallContext| { diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 0baf9ea8c..7be7f1ff5 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -216,6 +216,47 @@ describe('event sanitizer registries', () => { } }); + it('treats inline managed sanitizers as real flush barriers', async () => { + lib.registerSubscriber('node-event-inline-flush-sub', () => {}); + let blockerEntered; + const entered = new Promise((resolve) => { + blockerEntered = resolve; + }); + let releaseBlocker; + const release = new Promise((resolve) => { + releaseBlocker = resolve; + }); + let inlineFlushReturned = false; + + lib.registerMarkSanitizeGuardrail('node-event-inline-flush-blocker', 0, async (_event, fields) => { + blockerEntered(); + await release; + return fields; + }); + lib.registerScopeSanitizeStartGuardrail('node-event-inline-flush', 0, async (_event, fields) => { + await lib.flushSubscribers(); + inlineFlushReturned = true; + return fields; + }); + + try { + lib.event('inline-flush-blocker', null, { raw: true }); + await entered; + const execution = lib.toolCallExecute('inline-flush-tool', {}, (args) => args); + await new Promise((resolve) => setTimeout(resolve, 50)); + assert.equal(inlineFlushReturned, false); + releaseBlocker(); + await execution; + assert.equal(inlineFlushReturned, true); + await lib.flushSubscribers(); + } finally { + releaseBlocker(); + lib.deregisterMarkSanitizeGuardrail('node-event-inline-flush-blocker'); + lib.deregisterScopeSanitizeStartGuardrail('node-event-inline-flush'); + lib.deregisterSubscriber('node-event-inline-flush-sub'); + } + }); + it('clears sanitizer re-entrancy in async descendants after settlement', async () => { const events = capture('node-event-sanitize-descendant-flush-sub'); let secondSanitizerEntered; diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index 596e8ad7b..50ad75d3a 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -1368,6 +1368,101 @@ describe('LLM intercepts', () => { deregisterLlmStreamExecutionIntercept('node_llm_stream_exec_repl'); }); + it('snapshotted stream execution intercept survives deregistration', async () => { + let blockerEntered; + const entered = new Promise((resolve) => { + blockerEntered = resolve; + }); + let releaseBlocker; + const release = new Promise((resolve) => { + releaseBlocker = resolve; + }); + + registerLlmStreamExecutionIntercept('node_llm_stream_snapshot_target', 100, async (request, next) => [ + ...(await next(request)), + { snapshotted: true }, + ]); + registerLlmStreamExecutionIntercept('node_llm_stream_snapshot_blocker', -100, async (request, next) => { + blockerEntered(); + await release; + return next(request); + }); + + try { + const streamPromise = llmStreamCallExecute( + 'stream_snapshot_llm', + makeNative(), + (wrapper) => { + lib.pushStreamChunk(wrapper.__nemo_relay_stream_id, { downstream: true }); + lib.endStream(wrapper.__nemo_relay_stream_id); + }, + null, + null, + null, + null, + null, + null, + null, + ); + await entered; + assert.equal(deregisterLlmStreamExecutionIntercept('node_llm_stream_snapshot_target'), true); + releaseBlocker(); + const stream = await streamPromise; + const chunks = []; + for (;;) { + const chunk = await stream.next(); + if (chunk === null) { + break; + } + chunks.push(chunk); + } + assert.deepEqual(chunks, [{ downstream: true }, { snapshotted: true }]); + } finally { + releaseBlocker(); + deregisterLlmStreamExecutionIntercept('node_llm_stream_snapshot_blocker'); + deregisterLlmStreamExecutionIntercept('node_llm_stream_snapshot_target'); + } + }); + + it('completed deregistered stream intercepts do not keep Node alive', () => { + const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const script = ` + import { createRequire } from 'node:module'; + const require = createRequire(import.meta.url); + const lib = require(${modulePath}); + const request = { + headers: {}, + content: { messages: [], model: 'test-model' }, + }; + lib.registerLlmStreamExecutionIntercept('process-exit-stream', 10, async (value, next) => next(value)); + const stream = await lib.llmStreamCallExecute( + 'process-exit-llm', + request, + (wrapper) => { + lib.pushStreamChunk(wrapper.__nemo_relay_stream_id, { done: true }); + lib.endStream(wrapper.__nemo_relay_stream_id); + }, + null, + null, + null, + null, + null, + null, + null, + ); + while (await stream.next() !== null) {} + await stream.close(); + if (!lib.deregisterLlmStreamExecutionIntercept('process-exit-stream')) { + throw new Error('stream intercept was not deregistered'); + } + `; + + execFileSync(process.execPath, ['--input-type=module', '--eval', script], { + stdio: 'inherit', + timeout: 5_000, + }); + }); + it('stream execution intercept can return a single scalar chunk', async () => { registerLlmStreamExecutionIntercept('node_llm_stream_scalar', 10, async () => ({ scalar: true, @@ -1509,6 +1604,46 @@ describe('LLM intercepts', () => { assert.doesNotMatch(declarations, /registerLlmSanitizeRequestGuardrail\([^\n]*\.\.\.args: any\[\]/); }); + it('generated middleware declarations expose Promise-aware callback types', () => { + const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const registrations = [ + 'registerToolSanitizeRequestGuardrail', + 'registerToolSanitizeResponseGuardrail', + 'registerToolConditionalExecutionGuardrail', + 'registerToolRequestIntercept', + 'registerToolExecutionIntercept', + 'registerLlmSanitizeRequestGuardrail', + 'registerLlmSanitizeResponseGuardrail', + 'registerLlmConditionalExecutionGuardrail', + 'registerLlmRequestIntercept', + 'registerLlmExecutionIntercept', + 'registerLlmStreamExecutionIntercept', + 'scopeRegisterToolSanitizeRequestGuardrail', + 'scopeRegisterToolSanitizeResponseGuardrail', + 'scopeRegisterToolConditionalExecutionGuardrail', + 'scopeRegisterToolRequestIntercept', + 'scopeRegisterToolExecutionIntercept', + 'scopeRegisterLlmSanitizeRequestGuardrail', + 'scopeRegisterLlmSanitizeResponseGuardrail', + 'scopeRegisterLlmConditionalExecutionGuardrail', + 'scopeRegisterLlmRequestIntercept', + 'scopeRegisterLlmExecutionIntercept', + 'scopeRegisterLlmStreamExecutionIntercept', + ]; + + for (const registration of registrations) { + const declaration = declarations.match(new RegExp(`export declare function ${registration}\\([^\\n]+`))?.[0]; + assert.ok(declaration, `missing declaration for ${registration}`); + assert.doesNotMatch(declaration, /\.\.\.args: any\[\]/, `${registration} must not expose an any callback`); + assert.match(declaration, /Promise Promise').length - 1, + 2, + 'global and scope-local stream intercept declarations must expose the buffered next contract', + ); + }); + it('plugin declarations expose Promise middleware and the implemented stream contract', () => { const declarations = readFileSync(new URL('../plugin.d.ts', import.meta.url), 'utf8'); diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index a29d56131..79e4da5ba 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -790,7 +790,11 @@ pub enum NemoRelayNativeAsyncMiddlewareKind { LlmRequestIntercept = 8, /// LLM execution intercept with a continuation. LlmExecutionIntercept = 9, - /// Streaming LLM execution intercept with a continuation. + /// Reserved legacy discriminant for streaming LLM execution intercepts. + /// + /// Hosts reject this kind from the generic completion-based registration + /// hook. Use `plugin_context_register_async_stream_middleware` so chunks + /// remain incremental. LlmStreamExecutionIntercept = 10, /// Mark event sanitizer. MarkSanitize = 11, @@ -933,6 +937,8 @@ pub struct NemoRelayNativeHostApiV3 { pub async_completion_release: unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion), /// Invokes an execution continuation and settles a supplied completion. + /// + /// Cancellation of that completion aborts an in-flight continuation. pub async_next_invoke: unsafe extern "C" fn( next: *const NemoRelayNativeAsyncNext, invocation_json: *const NemoRelayNativeString, @@ -943,10 +949,12 @@ pub struct NemoRelayNativeHostApiV3 { /// Execution callbacks must call this exactly once after their final use /// for both `Complete` and `Pending` return states. pub async_next_release: unsafe extern "C" fn(next: *const NemoRelayNativeAsyncNext), - /// Registers any completion-based asynchronous middleware surface. + /// Registers a completion-based asynchronous middleware surface. /// /// `kind` must be a valid [`NemoRelayNativeAsyncMiddlewareKind`] - /// discriminant. The host rejects unknown `u32` values. + /// discriminant. The host rejects unknown `u32` values and + /// [`NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept`], + /// which must use `plugin_context_register_async_stream_middleware`. pub plugin_context_register_async_middleware: unsafe extern "C" fn( ctx: *mut NemoRelayNativePluginContext, kind: u32, @@ -957,7 +965,11 @@ pub struct NemoRelayNativeHostApiV3 { user_data: *mut c_void, free_fn: NemoRelayNativeFreeFn, ) -> NemoRelayStatus, - /// Pushes one JSON chunk to an incremental native stream. + /// Pushes one JSON chunk to an incremental native stream without blocking. + /// + /// A full bounded host queue returns [`NemoRelayStatus::Internal`] and + /// records a backpressure message in the host's last-error slot. The + /// producer may retry the logical chunk after the consumer advances. pub async_stream_push_json: unsafe extern "C" fn( stream: *const NemoRelayNativeAsyncStream, chunk_json: *const NemoRelayNativeString, @@ -965,7 +977,10 @@ pub struct NemoRelayNativeHostApiV3 { /// Finishes an incremental native stream successfully. pub async_stream_finish: unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> NemoRelayStatus, - /// Rejects an incremental native stream. + /// Rejects an incremental native stream without blocking. + /// + /// A full bounded queue returns [`NemoRelayStatus::Internal`]; the caller + /// may retry the rejection after the consumer advances. pub async_stream_reject: unsafe extern "C" fn( stream: *const NemoRelayNativeAsyncStream, message: *const NemoRelayNativeString, @@ -2485,6 +2500,8 @@ impl<'a> PluginContext<'a> { /// `cb`, `user_data`, and `free_fn` must remain valid until the host /// deregisters the callback or invokes `free_fn`. A callback returning /// `Pending` must settle and release its completion/next references. + /// [`NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept`] is + /// rejected; use [`Self::register_async_stream_middleware_raw`] instead. #[allow(clippy::too_many_arguments)] // Mirrors the native C ABI registration callback. pub unsafe fn register_async_middleware_raw( &mut self, @@ -2521,7 +2538,9 @@ impl<'a> PluginContext<'a> { /// # Safety /// The callback and user data must remain valid until deregistration or /// `free_fn`; callback-owned `next` and `stream` handles must each be - /// released exactly once. + /// released exactly once. Stream pushes and rejection are nonblocking: + /// `Internal` with a host last-error containing `backpressured` means the + /// bounded queue is full and the operation may be retried. pub unsafe fn register_async_stream_middleware_raw( &mut self, name: &str, diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 911f4b694..2a55ed5c4 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -15,6 +15,9 @@ use std::sync::Arc; use nemo_relay::api::llm as core_llm_api; use nemo_relay::api::llm::LlmAttributes; use nemo_relay::api::registry as core_registry_api; +use nemo_relay::api::runtime::subscriber_dispatcher::{ + with_publication_context, with_task_publication_context, +}; use nemo_relay::api::runtime::{ LlmExecutionNextFn, LlmJsonStream, LlmStreamExecutionNextFn, ToolExecutionNextFn, }; @@ -65,6 +68,10 @@ fn python_event_loop_running(py: Python<'_>) -> PyResult { } } +fn with_python_publication_context(f: impl FnOnce() -> T) -> T { + with_publication_context(py_callable::capture_python_publication_context(), f) +} + fn block_on_sync_middleware(future: F) -> FlowResult where F: Future> + Send, @@ -94,6 +101,7 @@ where C: Fn(Python<'_>, T) -> PyResult> + Send + 'static, { let scope_stack = current_scope_stack_handle(); + let publication_context = py_callable::capture_python_publication_context(); if !python_event_loop_running(py)? { let result = py .detach(|| { @@ -106,10 +114,12 @@ where return convert(py, result).map(|value| value.into_bound(py)); } pyo3_async_runtimes::tokio::future_into_py(py, async move { - let result = TASK_SCOPE_STACK - .scope(scope_stack, future) - .await - .map_err(to_py_err)?; + let result = with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, future), + ) + .await + .map_err(to_py_err)?; Python::attach(|py| convert(py, result)) }) } @@ -366,6 +376,7 @@ fn get_handle() -> PyResult { timestamp: "datetime.datetime | None"=None ) -> "ScopeHandle", text_signature = "(name: str, scope_type: ScopeType, *, handle: ScopeHandle | None = None, attributes: ScopeAttributes | None = None, data: object | None = None, metadata: object | None = None, input: object | None = None, timestamp: datetime.datetime | None = None) -> ScopeHandle")] fn push_scope( + _py: Python<'_>, name: &str, scope_type: PyScopeType, handle: Option, @@ -382,18 +393,20 @@ fn push_scope( let meta = opt_py_to_json(metadata)?; let input = opt_py_to_json(input)?; let timestamp = opt_py_to_timestamp(timestamp)?; - core_scope_api::push_scope( - core_scope_api::PushScopeParams::builder() - .name(name) - .scope_type(scope_type.into()) - .parent_opt(handle.as_ref().map(|h| &h.inner)) - .attributes(attrs) - .data_opt(d) - .metadata_opt(meta) - .input_opt(input) - .timestamp_opt(timestamp) - .build(), - ) + with_python_publication_context(|| { + core_scope_api::push_scope( + core_scope_api::PushScopeParams::builder() + .name(name) + .scope_type(scope_type.into()) + .parent_opt(handle.as_ref().map(|h| &h.inner)) + .attributes(attrs) + .data_opt(d) + .metadata_opt(meta) + .input_opt(input) + .timestamp_opt(timestamp) + .build(), + ) + }) .map(PyScopeHandle::from) .map_err(to_py_err) } @@ -416,6 +429,7 @@ fn push_scope( #[pyfunction] #[pyo3(signature = (handle: "ScopeHandle", output: "object | None"=None, metadata: "object | None"=None, timestamp: "datetime.datetime | None"=None) -> "None", text_signature = "(handle: ScopeHandle, output: object | None = None, metadata: object | None = None, timestamp: datetime.datetime | None = None) -> None")] fn pop_scope( + _py: Python<'_>, handle: &PyScopeHandle, output: Option<&Bound<'_, PyAny>>, metadata: Option<&Bound<'_, PyAny>>, @@ -424,14 +438,16 @@ fn pop_scope( let output = opt_py_to_json(output)?; let metadata = opt_py_to_json(metadata)?; let timestamp = opt_py_to_timestamp(timestamp)?; - core_scope_api::pop_scope( - core_scope_api::PopScopeParams::builder() - .handle_uuid(&handle.inner.uuid) - .output_opt(output) - .metadata_opt(metadata) - .timestamp_opt(timestamp) - .build(), - ) + with_python_publication_context(|| { + core_scope_api::pop_scope( + core_scope_api::PopScopeParams::builder() + .handle_uuid(&handle.inner.uuid) + .output_opt(output) + .metadata_opt(metadata) + .timestamp_opt(timestamp) + .build(), + ) + }) .map_err(to_py_err) } @@ -449,6 +465,7 @@ fn pop_scope( /// TypeError: If ``timestamp`` is not a ``datetime.datetime``. /// ValueError: If ``timestamp`` is a naive datetime. #[pyfunction] +#[allow(clippy::too_many_arguments)] #[pyo3(signature = ( name: "str", *, @@ -458,6 +475,7 @@ fn pop_scope( timestamp: "datetime.datetime | None"=None ) -> "None", text_signature = "(name: str, *, handle: ScopeHandle | None = None, data: object | None = None, metadata: object | None = None, timestamp: datetime.datetime | None = None) -> None")] fn event( + _py: Python<'_>, name: &str, handle: Option, data: Option<&Bound<'_, PyAny>>, @@ -467,15 +485,17 @@ fn event( let data = opt_py_to_json(data)?; let metadata = opt_py_to_json(metadata)?; let timestamp = opt_py_to_timestamp(timestamp)?; - core_scope_api::event( - core_scope_api::EmitMarkEventParams::builder() - .name(name) - .parent_opt(handle.as_ref().map(|h| &h.inner)) - .data_opt(data) - .metadata_opt(metadata) - .timestamp_opt(timestamp) - .build(), - ) + with_python_publication_context(|| { + core_scope_api::event( + core_scope_api::EmitMarkEventParams::builder() + .name(name) + .parent_opt(handle.as_ref().map(|h| &h.inner)) + .data_opt(data) + .metadata_opt(metadata) + .timestamp_opt(timestamp) + .build(), + ) + }) .map_err(to_py_err) } @@ -523,6 +543,7 @@ fn event( timestamp: "datetime.datetime | None"=None ) -> "ToolHandle", text_signature = "(name: str, args: object, *, handle: ScopeHandle | None = None, attributes: ToolAttributes | None = None, data: object | None = None, metadata: object | None = None, tool_call_id: str | None = None, timestamp: datetime.datetime | None = None) -> ToolHandle")] fn tool_call( + _py: Python<'_>, name: &str, args: &Bound<'_, PyAny>, handle: Option, @@ -539,18 +560,20 @@ fn tool_call( let data = opt_py_to_json(data)?; let metadata = opt_py_to_json(metadata)?; let timestamp = opt_py_to_timestamp(timestamp)?; - core_tool_api::tool_call( - core_tool_api::ToolCallParams::builder() - .name(name) - .args(args_json) - .parent_opt(handle.as_ref().map(|h| &h.inner)) - .attributes(attrs) - .data_opt(data) - .metadata_opt(metadata) - .tool_call_id_opt(tool_call_id) - .timestamp_opt(timestamp) - .build(), - ) + with_python_publication_context(|| { + core_tool_api::tool_call( + core_tool_api::ToolCallParams::builder() + .name(name) + .args(args_json) + .parent_opt(handle.as_ref().map(|h| &h.inner)) + .attributes(attrs) + .data_opt(data) + .metadata_opt(metadata) + .tool_call_id_opt(tool_call_id) + .timestamp_opt(timestamp) + .build(), + ) + }) .map(PyToolHandle::from) .map_err(to_py_err) } @@ -582,6 +605,7 @@ fn tool_call( timestamp: "datetime.datetime | None"=None ) -> "None", text_signature = "(handle: ToolHandle, result: object, *, data: object | None = None, metadata: object | None = None, timestamp: datetime.datetime | None = None) -> None")] fn tool_call_end( + _py: Python<'_>, handle: &PyToolHandle, result: &Bound<'_, PyAny>, data: Option<&Bound<'_, PyAny>>, @@ -592,15 +616,17 @@ fn tool_call_end( let data = opt_py_to_json(data)?; let metadata = opt_py_to_json(metadata)?; let timestamp = opt_py_to_timestamp(timestamp)?; - core_tool_api::tool_call_end( - core_tool_api::ToolCallEndParams::builder() - .handle(&handle.inner) - .result(result_json) - .data_opt(data) - .metadata_opt(metadata) - .timestamp_opt(timestamp) - .build(), - ) + with_python_publication_context(|| { + core_tool_api::tool_call_end( + core_tool_api::ToolCallEndParams::builder() + .handle(&handle.inner) + .result(result_json) + .data_opt(data) + .metadata_opt(metadata) + .timestamp_opt(timestamp) + .build(), + ) + }) .map_err(to_py_err) } @@ -660,9 +686,11 @@ fn tool_call_execute<'py>( let parent_handle = handle.map(|h| h.inner).unwrap_or_else(task_scope_top); let scope_stack = current_scope_stack_handle(); + let publication_context = py_callable::capture_python_publication_context(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { + with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, async move { let result = core_tool_api::tool_call_execute( core_tool_api::ToolCallExecuteParams::builder() .name(name) @@ -677,8 +705,9 @@ fn tool_call_execute<'py>( .await .map_err(to_py_err)?; Python::attach(|py| json_to_py(py, &result)) - }) - .await + }), + ) + .await }) } @@ -725,6 +754,7 @@ fn tool_call_execute<'py>( timestamp: "datetime.datetime | None"=None ) -> "LlmHandle", text_signature = "(name: str, request: LlmRequest, *, handle: ScopeHandle | None = None, attributes: LlmAttributes | None = None, data: object | None = None, metadata: object | None = None, model_name: str | None = None, timestamp: datetime.datetime | None = None) -> LlmHandle")] fn llm_call( + _py: Python<'_>, name: &str, request: PyLLMRequest, handle: Option, @@ -750,7 +780,7 @@ fn llm_call( .model_name_opt(model_name) .timestamp_opt(timestamp) .build(); - core_llm_api::llm_call(params) + with_python_publication_context(|| core_llm_api::llm_call(params)) .map(PyLLMHandle::from) .map_err(to_py_err) } @@ -777,6 +807,7 @@ fn llm_call( /// TypeError: If ``timestamp`` is not a ``datetime.datetime``. /// ValueError: If ``timestamp`` is a naive datetime. #[pyfunction] +#[allow(clippy::too_many_arguments)] #[pyo3(signature = ( handle: "LlmHandle", response: "object", @@ -788,6 +819,7 @@ fn llm_call( timestamp: "datetime.datetime | None"=None ) -> "None", text_signature = "(handle: LlmHandle, response: object, *, data: object | None = None, metadata: object | None = None, annotated_response: AnnotatedLLMResponse | object | None = None, response_codec: object | None = None, timestamp: datetime.datetime | None = None) -> None")] fn llm_call_end( + _py: Python<'_>, handle: &PyLLMHandle, response: &Bound<'_, PyAny>, data: Option<&Bound<'_, PyAny>>, @@ -802,17 +834,19 @@ fn llm_call_end( let response_codec = py_llm_response_codec(response_codec); let annotated_response = py_annotated_llm_response(annotated_response)?; let timestamp = opt_py_to_timestamp(timestamp)?; - core_llm_api::llm_call_end( - core_llm_api::LlmCallEndParams::builder() - .handle(&handle.inner) - .response(response_json) - .data_opt(data) - .metadata_opt(metadata) - .annotated_response_opt(annotated_response) - .response_codec_opt(response_codec) - .timestamp_opt(timestamp) - .build(), - ) + with_python_publication_context(|| { + core_llm_api::llm_call_end( + core_llm_api::LlmCallEndParams::builder() + .handle(&handle.inner) + .response(response_json) + .data_opt(data) + .metadata_opt(metadata) + .annotated_response_opt(annotated_response) + .response_codec_opt(response_codec) + .timestamp_opt(timestamp) + .build(), + ) + }) .map_err(to_py_err) } @@ -884,9 +918,11 @@ fn llm_call_execute<'py>( let response_codec_arc = py_llm_response_codec(response_codec); let scope_stack = current_scope_stack_handle(); + let publication_context = py_callable::capture_python_publication_context(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { + with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, async move { let params = core_llm_api::LlmCallExecuteParams::builder() .name(name) .request(request.inner) @@ -903,8 +939,9 @@ fn llm_call_execute<'py>( .await .map_err(to_py_err)?; Python::attach(|py| json_to_py(py, &result)) - }) - .await + }), + ) + .await }) } @@ -985,9 +1022,12 @@ fn llm_stream_call_execute<'py>( let response_codec_arc = py_llm_response_codec(response_codec); let scope_stack = current_scope_stack_handle(); + let publication_context = py_callable::capture_python_publication_context(); + let stream_publication_context = publication_context.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { + with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, async move { let params = core_llm_api::LlmStreamCallExecuteParams::builder() .name(name) .request(request.inner) @@ -1010,11 +1050,9 @@ fn llm_stream_call_execute<'py>( let (tx, rx) = tokio::sync::mpsc::channel::>(32); let (cancel, cancel_rx) = tokio::sync::watch::channel(false); let (closed, closed_rx) = tokio::sync::watch::channel(None); - tokio::spawn(forward_stream_to_channel( - rust_stream, - tx, - cancel_rx, - closed, + tokio::spawn(with_task_publication_context( + stream_publication_context, + forward_stream_to_channel(rust_stream, tx, cancel_rx, closed), )); Ok(PyLlmStream { @@ -1022,8 +1060,9 @@ fn llm_stream_call_execute<'py>( cancel, closed: closed_rx, }) - }) - .await + }), + ) + .await }) } @@ -1490,6 +1529,24 @@ fn flush_subscribers(py: Python<'_>) -> PyResult<()> { .map_err(to_py_err) } +/// Lock dispatcher resources before a process forks. +#[pyfunction] +fn subscriber_dispatcher_before_fork() { + nemo_relay::api::runtime::subscriber_dispatcher::prepare_for_fork(); +} + +/// Unlock dispatcher resources in the parent after a process forks. +#[pyfunction] +fn subscriber_dispatcher_after_fork_parent() { + nemo_relay::api::runtime::subscriber_dispatcher::resume_after_fork_parent(); +} + +/// Reset inherited dispatcher resources in the child after a process forks. +#[pyfunction] +fn subscriber_dispatcher_after_fork_child() { + nemo_relay::api::runtime::subscriber_dispatcher::reset_after_fork_child(); +} + // --------------------------------------------------------------------------- // Scope-local guardrail registrations (macro-generated) // --------------------------------------------------------------------------- @@ -1997,6 +2054,12 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(register_subscriber, m)?)?; m.add_function(wrap_pyfunction!(deregister_subscriber, m)?)?; m.add_function(wrap_pyfunction!(flush_subscribers, m)?)?; + m.add_function(wrap_pyfunction!(subscriber_dispatcher_before_fork, m)?)?; + m.add_function(wrap_pyfunction!( + subscriber_dispatcher_after_fork_parent, + m + )?)?; + m.add_function(wrap_pyfunction!(subscriber_dispatcher_after_fork_child, m)?)?; // Scope-local tool guardrails m.add_function(wrap_pyfunction!( diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 8260af6c6..96d58e15d 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -25,6 +25,7 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use nemo_relay::api::runtime::subscriber_dispatcher::{PublicationContext, publication_context}; use nemo_relay::api::runtime::{ EventSanitizeFn, EventSubscriberFn, LlmConditionalFn, LlmExecutionNextFn, LlmJsonStream, LlmRequestInterceptFn, LlmSanitizeRequestContext, LlmSanitizeRequestFn, @@ -204,23 +205,35 @@ fn capture_python_task_locals() -> Option { Python::attach(|py| pyo3_async_runtimes::tokio::get_current_locals(py).ok()) } -fn task_locals_with_running_loop(registered: Option<&TaskLocals>) -> Option { +struct PythonPublicationContext(TaskLocals); + +pub(crate) fn capture_python_publication_context() -> Option { capture_python_task_locals() - .or_else(|| registered.cloned()) - .filter(|locals| { - Python::attach(|py| { - let event_loop = locals.event_loop(py); - let running = event_loop - .call_method0("is_running") - .and_then(|value| value.extract::()) - .unwrap_or(false); - let closed = event_loop - .call_method0("is_closed") - .and_then(|value| value.extract::()) - .unwrap_or(true); - running && !closed - }) - }) + .map(PythonPublicationContext) + .map(|context| Arc::new(context) as PublicationContext) +} + +fn running_task_locals(locals: TaskLocals) -> Option { + let live = Python::attach(|py| { + let event_loop = locals.event_loop(py); + let running = event_loop + .call_method0("is_running") + .and_then(|value| value.extract::()) + .unwrap_or(false); + let closed = event_loop + .call_method0("is_closed") + .and_then(|value| value.extract::()) + .unwrap_or(true); + running && !closed + }); + live.then_some(locals) +} + +fn task_locals_with_running_loop(registered: Option<&TaskLocals>) -> Option { + publication_context::() + .and_then(|context| running_task_locals(context.0.clone())) + .or_else(|| capture_python_task_locals().and_then(running_task_locals)) + .or_else(|| registered.cloned().and_then(running_task_locals)) } async fn resolve_py_object_or_future( diff --git a/crates/python/tests/coverage/py_api_coverage_tests.rs b/crates/python/tests/coverage/py_api_coverage_tests.rs index 8f3b0ee5e..7f2af7995 100644 --- a/crates/python/tests/coverage/py_api_coverage_tests.rs +++ b/crates/python/tests/coverage/py_api_coverage_tests.rs @@ -77,6 +77,7 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { let data = py_dict(py, json!({"payload": true})); let metadata = py_dict(py, json!({"meta": true})); let child = push_scope( + py, "child", PyScopeType::Tool, Some(handle.clone()), @@ -92,6 +93,7 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { assert_eq!(child.inner.name, "child"); event( + py, "mark", Some(child.clone()), Some(&py_dict(py, json!({"step": 1}))), @@ -101,6 +103,7 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { .unwrap(); let tool = tool_call( + py, "tool", &py_dict(py, json!({"arg": 1})), Some(child.clone()), @@ -114,6 +117,7 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { ) .unwrap(); tool_call_end( + py, &tool, &py_dict(py, json!({"result": 2})), Some(&py_dict(py, json!({"done": true}))), @@ -129,6 +133,7 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { }, }; let llm = llm_call( + py, "llm", llm_request, Some(child.clone()), @@ -143,6 +148,7 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { ) .unwrap(); llm_call_end( + py, &llm, &py_dict(py, json!({"response": "ok"})), Some(&py_dict(py, json!({"tokens": 10}))), @@ -153,7 +159,7 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { ) .unwrap(); - pop_scope(&child, None, None, None).unwrap(); + pop_scope(py, &child, None, None, None).unwrap(); assert_eq!(get_handle().unwrap().inner.name, "root"); }); } @@ -387,6 +393,7 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute set_thread_scope_stack(&stack); let root = get_handle().unwrap(); let child = push_scope( + py, "child-exec", PyScopeType::Agent, Some(root.clone()), @@ -889,7 +896,7 @@ async def run_stream(api, request, func, collector, finalizer, handle, attribute assert!(deregister_subscriber(&global_subscriber).unwrap()); assert!(!deregister_subscriber(&global_subscriber).unwrap()); - pop_scope(&child, None, None, None).unwrap(); + pop_scope(py, &child, None, None, None).unwrap(); }); } diff --git a/docs/about-nemo-relay/concepts/middleware.mdx b/docs/about-nemo-relay/concepts/middleware.mdx index f74a3bb89..ee2e72638 100644 --- a/docs/about-nemo-relay/concepts/middleware.mdx +++ b/docs/about-nemo-relay/concepts/middleware.mdx @@ -25,16 +25,17 @@ All middleware families accept asynchronous callbacks. Rust callbacks return a future, and Node callbacks may return a value or a Promise. Python registrations accept callbacks that return a value or an awaitable when invoked through an asynchronous Relay API or queued event publication. Synchronous standalone -Python helpers cannot drive an awaitable callback and raise an error directing -the caller to the corresponding asynchronous helper. Relay awaits entries -sequentially in priority order, so later callbacks observe earlier middleware -output. - -Managed execution and standalone conditional/request-intercept helpers are -asynchronous because their result depends on middleware completion. Manual -lifecycle APIs (`tool_call`, `tool_call_end`, `llm_call`, and `llm_call_end`) -remain synchronous: they create or close their handle immediately and queue -observability work rather than awaiting it. +Python calls cannot drive an awaitable callback. Call the same standalone +helper from a running event loop and await the returned value instead. Relay +awaits entries sequentially in priority order, so later callbacks observe +earlier middleware output. + +Managed execution is asynchronous because its result depends on middleware +completion. Python standalone conditional and request-intercept helpers return +a direct value outside an event loop and an awaitable inside one. Manual lifecycle +APIs (`tool_call`, `tool_call_end`, `llm_call`, and `llm_call_end`) remain +synchronous: they create or close their handle immediately and queue observability +work rather than awaiting it. ## Registration Levels diff --git a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx index 8bb88a328..1429f47c0 100644 --- a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx +++ b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx @@ -148,12 +148,24 @@ Event sanitizers registered through this extension still run on Relay's serial publication dispatcher. Scope and mark emission remain synchronous and their sanitized events are delivered later in emission order. -The v3 completion and continuation ABI settles one JSON value. Consequently, -an async native LLM stream execution intercept currently receives and returns -the complete JSON array of chunks: Relay buffers the provider stream before -replaying it to the caller. It is not an incremental streaming transport and -does not provide per-chunk backpressure. Use a synchronous native stream -intercept or a worker plugin when first-token latency is required. +The generic v3 completion registration settles one JSON value and rejects the +`LlmStreamExecutionIntercept` kind. Register asynchronous stream intercepts +with `plugin_context_register_async_stream_middleware` instead. Its dedicated +`async_next_invoke_stream` continuation forwards downstream chunks +incrementally, so Relay does not buffer the provider stream into an array. + +The incremental output queue is bounded. `async_stream_push_json` and +`async_stream_reject` never block a native callback thread. If either returns +`Internal` and the host last-error contains `backpressured`, retain the logical +chunk or rejection and retry after the consumer advances. `InvalidArg` means +the stream is already closed or cancelled and must not be retried. Check +`async_stream_is_cancelled` during longer producer work. + +Cancelling the one-shot completion supplied to a non-stream execution +intercept also aborts any pending `async_next_invoke` continuation. Plugins +must still release their callback-owned completion and `next` references +exactly once; cancellation only stops the host-side continuation and prevents +it from retaining the plugin indefinitely. Legacy v1/v2 middleware callbacks are synchronous and run on the runtime's execution path. They must not block on I/O; use the v3 completion-based API for diff --git a/python/nemo_relay/_native.pyi b/python/nemo_relay/_native.pyi index d4f409405..3b516aca6 100644 --- a/python/nemo_relay/_native.pyi +++ b/python/nemo_relay/_native.pyi @@ -1985,6 +1985,18 @@ def flush_subscribers() -> None: """ ... +def subscriber_dispatcher_before_fork() -> None: + """Lock subscriber dispatcher resources before a process forks.""" + ... + +def subscriber_dispatcher_after_fork_parent() -> None: + """Unlock subscriber dispatcher resources in the parent after a fork.""" + ... + +def subscriber_dispatcher_after_fork_child() -> None: + """Reset and unlock inherited subscriber dispatcher resources in a child.""" + ... + def scope_register_tool_sanitize_request_guardrail( scope_uuid: str, name: str, priority: int, guardrail: _ToolSanitizeGuardrail ) -> None: diff --git a/python/nemo_relay/llm.py b/python/nemo_relay/llm.py index a2890de01..4a9cfd269 100644 --- a/python/nemo_relay/llm.py +++ b/python/nemo_relay/llm.py @@ -386,8 +386,10 @@ def request_intercepts(name, request): intercept chain. Returns: - LLMRequestInterceptOutcome: The complete request, annotation, and - pending-mark outcome produced by the intercept chain. + LLMRequestInterceptOutcome | Awaitable[LLMRequestInterceptOutcome]: + The complete request, annotation, and pending-mark outcome produced by + the intercept chain. Outside a running event loop this is returned + directly. Inside an event loop, await the returned value. Notes: This runs only the request-intercept chain. It does not execute @@ -405,12 +407,16 @@ def conditional_execution(request): conditional-execution guardrails. Returns: - str | None: A rejection message if execution should be blocked, - otherwise ``None``. + None | Awaitable[None]: ``None`` when execution is allowed, returned + directly outside an event loop or through an awaitable inside one. Notes: This helper evaluates only conditional-execution guardrails and does not invoke request intercepts, codecs, or provider execution. + + Raises: + RuntimeError: If a guardrail rejects the call or an asynchronous + guardrail is registered when called outside an event loop. """ ensure_scope_stack() return _native_llm_conditional_execution(request) diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index f0fb72339..377bee552 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -23,8 +23,9 @@ def log_event(event): """ import asyncio +import os +import threading from collections.abc import Callable -from concurrent.futures import ThreadPoolExecutor from typing import TYPE_CHECKING from nemo_relay._event_sanitizer_context import callback_active as _event_sanitizer_callback_active @@ -37,11 +38,25 @@ def log_event(event): from nemo_relay._native import ( register_subscriber as _native_register, ) +from nemo_relay._native import ( + subscriber_dispatcher_after_fork_child as _native_after_fork_child, +) +from nemo_relay._native import ( + subscriber_dispatcher_after_fork_parent as _native_after_fork_parent, +) +from nemo_relay._native import ( + subscriber_dispatcher_before_fork as _native_before_fork, +) if TYPE_CHECKING: from nemo_relay import Event -_FLUSH_EXECUTOR = ThreadPoolExecutor(max_workers=1, thread_name_prefix="nemo-relay-flush") +if hasattr(os, "register_at_fork"): + os.register_at_fork( + before=_native_before_fork, + after_in_parent=_native_after_fork_parent, + after_in_child=_native_after_fork_child, + ) def register(name: str, callback: "Callable[[Event], None]") -> None: @@ -124,14 +139,41 @@ def flush() -> None: async def flush_async() -> None: """Wait asynchronously for subscriber callbacks already queued by Relay. - Use this barrier from an ``asyncio`` task. The blocking native wait runs on - Relay's dedicated flush thread so an event sanitizer scheduled on the - caller's event loop or default executor can continue to make progress. + Use this barrier from an ``asyncio`` task. A daemon bridge thread waits for + the native dispatcher without blocking the Python event loop or process + shutdown when this coroutine is cancelled. """ if _event_sanitizer_callback_active(): return None loop = asyncio.get_running_loop() - await loop.run_in_executor(_FLUSH_EXECUTOR, _native_flush) + completed: asyncio.Future[None] = loop.create_future() + + def finish(error: BaseException | None) -> None: + if completed.done(): + return + if error is None: + completed.set_result(None) + else: + completed.set_exception(error) + + def wait_for_dispatcher() -> None: + try: + _native_flush() + except BaseException as error: + result = error + else: + result = None + try: + loop.call_soon_threadsafe(finish, result) + except RuntimeError: + pass + + threading.Thread( + target=wait_for_dispatcher, + name="nemo-relay-flush", + daemon=True, + ).start() + await completed __all__ = ["deregister", "flush", "flush_async", "register"] diff --git a/python/nemo_relay/tools.py b/python/nemo_relay/tools.py index 9c1d364b4..a3218136d 100644 --- a/python/nemo_relay/tools.py +++ b/python/nemo_relay/tools.py @@ -196,7 +196,9 @@ def request_intercepts(name, args): args: JSON-compatible tool arguments to pass through the intercepts. Returns: - Json: The arguments produced by the final request intercept. + Json | Awaitable[Json]: The arguments produced by the final request + intercept. Outside a running event loop this is returned directly. + Inside an event loop, await the returned value. Notes: This runs only the request-intercept chain. It does not execute @@ -214,12 +216,16 @@ def conditional_execution(name, args): args: JSON-compatible tool arguments to validate. Returns: - str | None: A rejection message if execution should be blocked, - otherwise ``None``. + None | Awaitable[None]: ``None`` when execution is allowed, returned + directly outside an event loop or through an awaitable inside one. Notes: This helper evaluates only the conditional-execution guardrail chain and does not invoke request intercepts or tool execution. + + Raises: + RuntimeError: If a guardrail rejects the call or an asynchronous + guardrail is registered when called outside an event loop. """ ensure_scope_stack() return _native_tool_conditional_execution(name, args) diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 1aadb30a1..652550de5 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio +import contextvars from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor from typing import cast @@ -98,6 +99,32 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert events[-1].data == {"async": True} +async def test_async_mark_sanitizer_uses_each_emitter_context(capture_events): + request_id = contextvars.ContextVar("request_id", default="registration") + observed: dict[str, str] = {} + + async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + await asyncio.sleep(0) + observed[event.name] = request_id.get() + return fields + + async def emit(name: str) -> None: + token = request_id.set(name) + try: + scope.event(name) + finally: + request_id.reset(token) + + guardrails.register_mark_sanitize("python-emitter-context", 0, sanitize) + try: + await asyncio.gather(emit("request-a"), emit("request-b")) + await subscribers.flush_async() + finally: + guardrails.deregister_mark_sanitize("python-emitter-context") + + assert observed == {"request-a": "request-a", "request-b": "request-b"} + + async def test_async_flush_keeps_originating_sanitizer_loop_running(capture_events): _capture_name, events = capture_events diff --git a/python/tests/test_subscribers.py b/python/tests/test_subscribers.py index 076028861..0b77e2980 100644 --- a/python/tests/test_subscribers.py +++ b/python/tests/test_subscribers.py @@ -3,6 +3,10 @@ """Tests for NeMo Relay subscriber and event handling.""" +import os +import subprocess +import sys +import textwrap import threading import time from datetime import datetime, timezone @@ -98,6 +102,80 @@ def test_duplicate_subscriber_raises(self): def test_deregister_nonexistent(self): assert not subscribers.deregister("nonexistent_sub") + @pytest.mark.skipif(not hasattr(os, "fork"), reason="requires os.fork") + def test_async_flush_remains_usable_after_fork(self): + script = textwrap.dedent( + """ + import asyncio + import os + + from nemo_relay import subscribers + + asyncio.run(subscribers.flush_async()) + child = os.fork() + if child == 0: + async def flush(): + await asyncio.wait_for(subscribers.flush_async(), timeout=1) + + try: + asyncio.run(flush()) + except BaseException: + os._exit(42) + os._exit(0) + + _, status = os.waitpid(child, 0) + raise SystemExit(os.waitstatus_to_exitcode(status)) + """ + ) + completed = subprocess.run( + [sys.executable, "-c", script], + check=False, + capture_output=True, + text=True, + timeout=5, + ) + assert completed.returncode == 0, completed.stderr + + def test_cancelled_async_flush_does_not_block_process_exit(self): + script = textwrap.dedent( + """ + import asyncio + import threading + + from nemo_relay import guardrails, scope, subscribers + + entered = threading.Event() + + async def stuck(_event, fields): + entered.set() + await asyncio.Event().wait() + return fields + + subscribers.register("cancel-flush-sink", lambda _event: None) + guardrails.register_mark_sanitize("cancel-flush-sanitizer", 0, stuck) + scope.event("cancel-flush") + + async def run(): + flush = asyncio.create_task(subscribers.flush_async()) + assert await asyncio.to_thread(entered.wait, 2) + flush.cancel() + try: + await flush + except asyncio.CancelledError: + pass + + asyncio.run(run()) + """ + ) + completed = subprocess.run( + [sys.executable, "-c", script], + check=False, + capture_output=True, + text=True, + timeout=5, + ) + assert completed.returncode == 0, completed.stderr + class TestSubscriberEventDetails: def test_scope_events_have_correct_types(self): From b22299fd190c06de953218cd47424c9d1b0e32b6 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 02:17:15 -0400 Subject: [PATCH 43/83] fix: make async publication context safe Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 49 ++-- crates/core/src/api/runtime/state.rs | 34 +-- .../src/api/runtime/subscriber_dispatcher.rs | 143 ++++++---- crates/core/src/api/shared.rs | 18 +- crates/core/src/api/tool.rs | 28 +- crates/core/src/plugin/dynamic/native.rs | 9 +- crates/core/src/plugin/dynamic/worker.rs | 32 ++- crates/core/src/stream.rs | 12 +- .../tests/integration/middleware_tests.rs | 95 ++++++- .../core/tests/unit/dynamic_worker_tests.rs | 66 +++++ crates/core/tests/unit/native_plugin_tests.rs | 59 ++++ crates/node/src/api/mod.rs | 264 ++++++++++-------- crates/node/src/callback_factory.rs | 31 +- crates/node/src/promise_call.rs | 22 +- crates/node/tests/event_sanitizers_tests.mjs | 53 +++- crates/python/src/py_callable.rs | 106 +++++-- python/nemo_relay/subscribers.py | 114 ++++++-- python/tests/test_event_sanitizers.py | 22 ++ python/tests/test_subscribers.py | 66 ++++- 19 files changed, 907 insertions(+), 316 deletions(-) diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index cfd107173..3b36b6aa3 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -33,8 +33,8 @@ use crate::api::scope::event; use crate::api::scope::{EmitMarkEventParams, ScopeHandle}; use crate::api::shared::{ ensure_runtime_owner, inject_dynamo_session_ids, metadata_with_otel_status, - resolve_parent_uuid, run_request_intercepts_with_codec_and_recorder, - sanitize_event_with_scope_stack, snapshot_event_sanitizers, snapshot_event_subscribers, + resolve_parent_uuid, run_request_intercepts_with_codec_and_recorder, snapshot_event_sanitizers, + snapshot_event_subscribers, }; use crate::codec::request::{AnnotatedLlmRequest, Message}; use crate::codec::response::{AnnotatedLlmResponse, attach_estimated_cost_for_provider}; @@ -57,6 +57,15 @@ const OBSERVABILITY_CREDENTIAL_HEADERS: [&str; 7] = [ "x-goog-api-key", ]; +fn queue_sanitized_event_with_scope_stack( + event: Event, + subscribers: &[EventSubscriberFn], + scope_stack: &ScopeStackHandle, +) -> bool { + let sanitizers = snapshot_event_sanitizers(&event, scope_stack).unwrap_or_default(); + dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack.clone()) +} + #[derive(Clone)] struct CapturedLlmScopeStack(ScopeStackHandle); @@ -464,9 +473,7 @@ async fn emit_llm_start_with_subscribers( .map_err(|error| FlowError::Internal(error.to_string()))?; state.build_llm_start_event(handle, input, annotated_request) }; - if let Some(event) = sanitize_event_with_scope_stack(event, scope_stack).await { - NemoRelayContextState::emit_event(&event, subscribers); - } + queue_sanitized_event_with_scope_stack(event, subscribers, scope_stack); Ok(()) } @@ -528,11 +535,7 @@ async fn emit_pending_request_marks( mark.category, mark.category_profile, )); - if let Some(event) = - sanitize_event_with_scope_stack(event, handle.captured_scope_stack()).await - { - NemoRelayContextState::emit_event(&event, subscribers); - } + queue_sanitized_event_with_scope_stack(event, subscribers, handle.captured_scope_stack()); } Ok(()) } @@ -541,8 +544,14 @@ pub(crate) async fn emit_optimization_marks(handle: &LlmHandle, subscribers: &[E emit_optimization_marks_with_async( handle, subscribers, - |event| sanitize_event_with_scope_stack(event, handle.captured_scope_stack()), - |event, subscribers| NemoRelayContextState::try_emit_event(event, subscribers), + |event| async { Some(event) }, + |event, subscribers| { + queue_sanitized_event_with_scope_stack( + event.clone(), + subscribers, + handle.captured_scope_stack(), + ) + }, ) .await; } @@ -554,11 +563,13 @@ pub(crate) async fn emit_reserved_optimization_marks( emit_optimization_marks_with_async( handle, subscribers, - |event| sanitize_event_with_scope_stack(event, handle.captured_scope_stack()), + |event| async { Some(event) }, |event, subscribers| { + let sanitizers = + snapshot_event_sanitizers(event, handle.captured_scope_stack()).unwrap_or_default(); dispatch_reserved_sanitized_event( event.clone(), - Vec::new(), + sanitizers, subscribers, handle.captured_scope_stack().clone(), ) @@ -1076,10 +1087,7 @@ async fn llm_call_end_with_behavior( .build(), ) }; - if let Some(event) = sanitize_event_with_scope_stack(event, handle.captured_scope_stack()).await - { - NemoRelayContextState::emit_event(&event, &subscribers); - } + queue_sanitized_event_with_scope_stack(event, &subscribers, handle.captured_scope_stack()); if let Some(error) = payload.decode_error && behavior.response_codec_errors_fatal { @@ -1193,10 +1201,7 @@ async fn emit_llm_end_without_output( .map_err(|error| FlowError::Internal(error.to_string()))?; state.end_llm_handle(handle, data, metadata, annotated_response) }; - if let Some(event) = sanitize_event_with_scope_stack(event, handle.captured_scope_stack()).await - { - NemoRelayContextState::emit_event(&event, &subscribers); - } + queue_sanitized_event_with_scope_stack(event, &subscribers, handle.captured_scope_stack()); Ok(()) } diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index b83e3d170..2ed3a9c79 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -32,7 +32,7 @@ use crate::api::runtime::callbacks::{ }; use crate::api::runtime::subscriber_dispatcher; use crate::api::scope::{CreateScopeHandleParams, EndScopeHandleParams, ScopeHandle, ScopeType}; -use crate::api::shared::sanitize_event; +use crate::api::shared::snapshot_event_sanitizers; use crate::api::tool::ToolHandle; use crate::api::tool::{ CreateToolHandleParams, EndToolHandleParams, ToolExecutionInterceptOutcome, @@ -194,19 +194,11 @@ impl NemoRelayContextState { /// # Parameters /// - `event`: Fully constructed lifecycle event to deliver. /// - `subscribers`: Subscribers that should observe the event. + #[cfg(test)] pub(crate) fn emit_event(event: &Event, subscribers: &[EventSubscriberFn]) { let _ = subscriber_dispatcher::dispatch_event(event, subscribers); } - /// Queue an event and report whether the asynchronous dispatcher accepted it. - /// - /// Subscriber callbacks still run asynchronously. This acknowledgement only - /// covers queue acceptance and is used by bounded observability cursors so a - /// transient dispatcher failure does not permanently discard evidence. - pub(crate) fn try_emit_event(event: &Event, subscribers: &[EventSubscriberFn]) -> bool { - subscriber_dispatcher::dispatch_event(event, subscribers) - } - /// Build a standalone mark event. /// /// # Parameters @@ -595,9 +587,14 @@ impl NemoRelayContextState { EventCategory::from(handle.scope_type), None, )); - if let Some(event) = sanitize_event(event).await { - Self::emit_event(&event, subscribers); - } + let scope_stack = super::current_scope_stack(); + let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + subscriber_dispatcher::dispatch_sanitized_event( + event, + sanitizers, + subscribers, + scope_stack, + ); handle } @@ -620,9 +617,14 @@ impl NemoRelayContextState { EventCategory::from(handle.scope_type), None, )); - if let Some(event) = sanitize_event(event).await { - Self::emit_event(&event, subscribers); - } + let scope_stack = super::current_scope_stack(); + let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + subscriber_dispatcher::dispatch_sanitized_event( + event, + sanitizers, + subscribers, + scope_stack, + ); } /// Snapshot event sanitizer entries in priority order. diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 5e8412944..760d8d844 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -79,14 +79,16 @@ pub(crate) type EventTransformFn = Box< mod native { use std::cell::{Cell, RefCell}; use std::panic::{AssertUnwindSafe, catch_unwind}; - use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Mutex; + use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; - use std::sync::{LazyLock, Mutex, MutexGuard}; use super::*; + #[cfg(test)] + use crate::api::runtime::scope_stack::current_scope_stack; use crate::api::runtime::scope_stack::{ - ScopeStackHandle, capture_thread_scope_stack, current_scope_stack, - restore_thread_scope_stack, set_thread_scope_stack, + ScopeStackHandle, capture_thread_scope_stack, restore_thread_scope_stack, + set_thread_scope_stack, }; use crate::error::FlowError; @@ -110,14 +112,31 @@ mod native { type DispatcherState = Option, String>>; type SanitizerRuntimeState = Option>; - static DISPATCHER: LazyLock> = LazyLock::new(|| Mutex::new(None)); - static SANITIZER_RUNTIME: LazyLock> = - LazyLock::new(|| Mutex::new(None)); - static DISPATCHER_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); - static SANITIZER_RUNTIME_FAILURE_LOGGED: AtomicBool = AtomicBool::new(false); + struct ProcessState { + dispatcher: Mutex, + sanitizer_runtime: Mutex, + dispatcher_failure_logged: AtomicBool, + sanitizer_runtime_failure_logged: AtomicBool, + } + + impl ProcessState { + fn new() -> Self { + Self { + dispatcher: Mutex::new(None), + sanitizer_runtime: Mutex::new(None), + dispatcher_failure_logged: AtomicBool::new(false), + sanitizer_runtime_failure_logged: AtomicBool::new(false), + } + } + } + + // Process states are intentionally never reclaimed after becoming active. + // A forked child cannot safely drop the inherited state because another + // vanished parent thread may have held one of its mutexes at fork time. + static PROCESS_STATE: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); thread_local! { static IN_DISPATCHER: Cell = const { Cell::new(false) }; - static FORK_GUARDS: RefCell> = const { RefCell::new(None) }; + static PREPARED_FORK_STATE: Cell<*mut ProcessState> = const { Cell::new(std::ptr::null_mut()) }; } tokio::task_local! { static ASYNC_PUBLICATION_MESSAGES: RefCell>>; @@ -125,15 +144,30 @@ mod native { struct DispatchGuard; - struct ForkGuards { - sanitizer_runtime: MutexGuard<'static, SanitizerRuntimeState>, - dispatcher: MutexGuard<'static, DispatcherState>, - } - pub(crate) struct AsyncPublication { sender: Sender>, } + fn process_state() -> &'static ProcessState { + let mut state = PROCESS_STATE.load(Ordering::Acquire); + if state.is_null() { + let fresh = Box::into_raw(Box::new(ProcessState::new())); + match PROCESS_STATE.compare_exchange( + std::ptr::null_mut(), + fresh, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => state = fresh, + Err(existing) => { + state = existing; + unsafe { drop(Box::from_raw(fresh)) }; + } + } + } + unsafe { &*state } + } + impl DispatchGuard { fn enter() -> Self { IN_DISPATCHER.with(|flag| flag.set(true)); @@ -151,7 +185,8 @@ mod native { pub(super) fn block_on_sanitizer_future( future: F, ) -> std::result::Result { - let mut runtime = SANITIZER_RUNTIME + let mut runtime = process_state() + .sanitizer_runtime .lock() .unwrap_or_else(|error| error.into_inner()); let runtime = runtime.get_or_insert_with(build_sanitizer_runtime); @@ -168,6 +203,7 @@ mod native { .map_err(|error| error.to_string()) } + #[cfg(test)] pub(super) fn dispatch_event(event: &Event, subscribers: &[EventSubscriberFn]) -> bool { if subscribers.is_empty() { return true; @@ -276,7 +312,10 @@ mod native { return Ok(()); } let sender = { - let dispatcher = DISPATCHER.lock().unwrap_or_else(|error| error.into_inner()); + let dispatcher = process_state() + .dispatcher + .lock() + .unwrap_or_else(|error| error.into_inner()); let Some(sender_result) = dispatcher.as_ref() else { return Ok(()); }; @@ -327,7 +366,10 @@ mod native { } fn dispatcher_sender() -> std::result::Result, String> { - let mut dispatcher = DISPATCHER.lock().unwrap_or_else(|error| error.into_inner()); + let mut dispatcher = process_state() + .dispatcher + .lock() + .unwrap_or_else(|error| error.into_inner()); dispatcher.get_or_insert_with(start_dispatcher).clone() } @@ -343,7 +385,11 @@ mod native { ); false } - Err(error) if !DISPATCHER_FAILURE_LOGGED.swap(true, Ordering::AcqRel) => { + Err(error) + if !process_state() + .dispatcher_failure_logged + .swap(true, Ordering::AcqRel) => + { log::error!( target: "nemo_relay.runtime", event = "subscriber_dispatcher_failed"; @@ -459,14 +505,19 @@ mod native { sanitizers: Vec>, publication_context: Option, ) -> Option { - let mut runtime = SANITIZER_RUNTIME + let state = process_state(); + let mut runtime = state + .sanitizer_runtime .lock() .unwrap_or_else(|error| error.into_inner()); let runtime = runtime.get_or_insert_with(build_sanitizer_runtime); let runtime = match runtime.as_ref() { Ok(runtime) => runtime, Err(error) => { - if !SANITIZER_RUNTIME_FAILURE_LOGGED.swap(true, Ordering::AcqRel) { + if !state + .sanitizer_runtime_failure_logged + .swap(true, Ordering::AcqRel) + { log::error!( target: "nemo_relay.runtime", event = "event_sanitizer_runtime_failed"; @@ -520,41 +571,38 @@ mod native { } pub(super) fn prepare_for_fork() { - // Lock in the same order used by publication: sanitizer execution can - // enqueue another event and therefore acquire the dispatcher lock. - let sanitizer_runtime = SANITIZER_RUNTIME - .lock() - .unwrap_or_else(|error| error.into_inner()); - let dispatcher = DISPATCHER.lock().unwrap_or_else(|error| error.into_inner()); - FORK_GUARDS.with(|guards| { - let previous = guards.replace(Some(ForkGuards { - sanitizer_runtime, - dispatcher, - })); - assert!(previous.is_none(), "subscriber fork preparation is nested"); + // Allocate the child's fresh state before fork. Do not lock active + // dispatcher state here: a pending Python sanitizer may require the + // forking event-loop thread to make progress. + PREPARED_FORK_STATE.with(|prepared| { + assert!( + prepared.get().is_null(), + "subscriber fork preparation is nested" + ); + prepared.set(Box::into_raw(Box::new(ProcessState::new()))); }); } pub(super) fn resume_after_fork_parent() { - FORK_GUARDS.with(|guards| { - guards - .borrow_mut() - .take() - .expect("subscriber fork parent hook ran without preparation"); + PREPARED_FORK_STATE.with(|prepared| { + let state = prepared.replace(std::ptr::null_mut()); + assert!( + !state.is_null(), + "subscriber fork parent hook ran without preparation" + ); + unsafe { drop(Box::from_raw(state)) }; }); } pub(super) fn reset_after_fork_child() { - FORK_GUARDS.with(|guards| { - let mut guards = guards - .borrow_mut() - .take() - .expect("subscriber fork child hook ran without preparation"); - *guards.dispatcher = None; - *guards.sanitizer_runtime = None; + PREPARED_FORK_STATE.with(|prepared| { + let state = prepared.replace(std::ptr::null_mut()); + assert!( + !state.is_null(), + "subscriber fork child hook ran without preparation" + ); + PROCESS_STATE.store(state, Ordering::Release); }); - DISPATCHER_FAILURE_LOGGED.store(false, Ordering::Release); - SANITIZER_RUNTIME_FAILURE_LOGGED.store(false, Ordering::Release); } #[cfg(test)] @@ -698,6 +746,7 @@ pub(crate) fn block_on_sanitizer_future( } /// Queue an event for subscriber delivery. +#[cfg(test)] pub(crate) fn dispatch_event(event: &Event, subscribers: &[EventSubscriberFn]) -> bool { native::dispatch_event(event, subscribers) } diff --git a/crates/core/src/api/shared.rs b/crates/core/src/api/shared.rs index 40e22159e..d6eaf4b91 100644 --- a/crates/core/src/api/shared.rs +++ b/crates/core/src/api/shared.rs @@ -44,20 +44,6 @@ pub(crate) fn snapshot_event_subscribers( Ok(state.collect_event_subscribers(&scope_local_subscribers)) } -/// Apply the event sanitizer chain visible on the current scope stack. -pub(crate) async fn sanitize_event(event: Event) -> Option { - sanitize_event_with_scope_stack(event, ¤t_scope_stack()).await -} - -/// Apply the event sanitizer chain visible on a captured scope stack. -pub(crate) async fn sanitize_event_with_scope_stack( - event: Event, - scope_stack: &ScopeStackHandle, -) -> Option { - let entries = snapshot_event_sanitizers(&event, scope_stack).unwrap_or_default(); - Some(NemoRelayContextState::event_sanitize_snapshot_chain(event, &entries).await) -} - /// Snapshot the event sanitizers visible to an event without invoking them. /// /// Scope and mark emission use this to capture middleware ownership while the @@ -75,7 +61,7 @@ pub(crate) fn snapshot_event_sanitizers( log::error!( target: "nemo_relay.runtime", event = "event_sanitizer_snapshot_failed"; - "Event was dropped because the scope stack lock is poisoned: {error}" + "Event sanitizer snapshot failed open because the scope stack lock is poisoned; publishing without event sanitizers: {error}" ); return None; } @@ -87,7 +73,7 @@ pub(crate) fn snapshot_event_sanitizers( log::error!( target: "nemo_relay.runtime", event = "event_sanitizer_snapshot_failed"; - "Event was dropped because the runtime context lock is poisoned: {error}" + "Event sanitizer snapshot failed open because the runtime context lock is poisoned; publishing without event sanitizers: {error}" ); return None; } diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 14b974651..719f2bb58 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -14,7 +14,7 @@ use crate::api::runtime::{EventSubscriberFn, ToolExecutionNextFn, with_active_ev use crate::api::scope::event; use crate::api::scope::{EmitMarkEventParams, ScopeHandle}; use crate::api::shared::{ - ensure_runtime_owner, metadata_with_otel_status, resolve_parent_uuid, sanitize_event, + ensure_runtime_owner, metadata_with_otel_status, resolve_parent_uuid, snapshot_event_sanitizers, snapshot_event_subscribers, }; use crate::api::skill_load; @@ -27,6 +27,12 @@ use uuid::Uuid; pub use nemo_relay_types::api::tool::{ToolAttributes, ToolExecutionInterceptOutcome}; +fn queue_sanitized_event(event: Event, subscribers: &[EventSubscriberFn]) -> bool { + let scope_stack = current_scope_stack(); + let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack) +} + /// Runtime-owned handle identifying an active or completed tool call. #[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] #[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))] @@ -381,13 +387,9 @@ async fn tool_call_with_subscriber_snapshot( .collect::>(); (handle, event, marks) }; - if let Some(event) = sanitize_event(event).await { - NemoRelayContextState::emit_event(&event, &subscribers); - } + queue_sanitized_event(event, &subscribers); for mark in marks { - if let Some(mark) = sanitize_event(mark).await { - NemoRelayContextState::emit_event(&mark, &subscribers); - } + queue_sanitized_event(mark, &subscribers); } Ok((handle, subscribers)) } @@ -553,13 +555,9 @@ async fn tool_call_end_with_pending_marks( )) }) .collect::>(); - if let Some(event) = sanitize_event(event).await { - NemoRelayContextState::emit_event(&event, subscribers); - } + queue_sanitized_event(event, subscribers); for mark in marks { - if let Some(mark) = sanitize_event(mark).await { - NemoRelayContextState::emit_event(&mark, subscribers); - } + queue_sanitized_event(mark, subscribers); } Ok(()) } @@ -577,9 +575,7 @@ async fn emit_tool_end_without_output( .map_err(|error| FlowError::Internal(error.to_string()))?; state.end_tool_handle(handle, handle.data.clone(), metadata) }; - if let Some(event) = sanitize_event(event).await { - NemoRelayContextState::emit_event(&event, lifecycle_subscribers); - } + queue_sanitized_event(event, lifecycle_subscribers); Ok(()) } diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 095db85ad..68793b581 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -1386,6 +1386,7 @@ const NATIVE_ASYNC_STREAM_CHANNEL_CAPACITY: usize = 64; struct NativeAsyncCompletion { sender: Mutex>>>, cancelled: AtomicBool, + next_invoked: AtomicBool, next_abort: Mutex>, // A pending native callback can continue running after its completion // wakes the awaiting task. Keep the callback's dynamic-library instance @@ -1517,6 +1518,7 @@ async fn invoke_native_async_callback( let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), _callback_user_data: Some(user_data.clone()), }); @@ -1851,7 +1853,12 @@ unsafe extern "C" fn native_async_next_invoke( .next_abort .lock() .unwrap_or_else(|error| error.into_inner()); - if abort_guard.is_some() { + let unsettled = completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_some(); + if !unsettled || completion.next_invoked.swap(true, Ordering::AcqRel) { set_native_last_error("native async next was already invoked for this completion"); return NemoRelayStatus::InvalidArg; } diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 1d0bb2ecf..642bd1df1 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -1622,10 +1622,10 @@ impl WorkerPluginCallback { context.codec_capability_id = Some(capability_id.clone()); capability_id }); + let _capability_guard = capability_id.as_ref().map(|capability_id| { + WorkerCodecCapabilityGuard::new(Arc::clone(&self.host_state), capability_id.clone()) + }); let response = self.invoke_async(invoke).await; - if let Some(capability_id) = capability_id { - self.host_state.remove_codec(&capability_id); - } optional_json_from_invoke_response(response?)? .map(serde_json::from_value) .transpose() @@ -1672,10 +1672,10 @@ impl WorkerPluginCallback { context.codec_capability_id = Some(capability_id.clone()); capability_id }); + let _capability_guard = capability_id.as_ref().map(|capability_id| { + WorkerCodecCapabilityGuard::new(Arc::clone(&self.host_state), capability_id.clone()) + }); let response = self.invoke_async(invoke).await; - if let Some(capability_id) = capability_id { - self.host_state.remove_codec(&capability_id); - } optional_json_from_invoke_response(response?) } @@ -2075,6 +2075,26 @@ struct WorkerCodecCapability { direction: WorkerCodecDirection, } +struct WorkerCodecCapabilityGuard { + host_state: Arc, + capability_id: String, +} + +impl WorkerCodecCapabilityGuard { + fn new(host_state: Arc, capability_id: String) -> Self { + Self { + host_state, + capability_id, + } + } +} + +impl Drop for WorkerCodecCapabilityGuard { + fn drop(&mut self) { + self.host_state.remove_codec(&self.capability_id); + } +} + enum WorkerCodecDirection { Request(Arc), Response(Arc), diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index d28899d01..e9b042f50 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -43,9 +43,7 @@ use crate::api::runtime::subscriber_dispatcher; use crate::api::runtime::{ EventSubscriberFn, LlmJsonStream, LlmStreamInner, ScopeStackHandle, current_scope_stack, }; -use crate::api::shared::{ - metadata_with_otel_status, sanitize_event_with_scope_stack, snapshot_event_sanitizers, -}; +use crate::api::shared::{metadata_with_otel_status, snapshot_event_sanitizers}; use crate::codec::response::{AnnotatedLlmResponse, attach_estimated_cost_for_provider}; use crate::codec::traits::LlmResponseCodec; use crate::error::{FlowError, Result}; @@ -324,12 +322,12 @@ impl LlmStreamWrapper { Err(_) => None, } }; - if let Some(event) = event_snapshot - && let Some(event) = sanitize_event_with_scope_stack(event, &scope_stack).await - { + if let Some(event) = event_snapshot { + let sanitizers = + snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); let _ = subscriber_dispatcher::dispatch_reserved_sanitized_event( event, - Vec::new(), + sanitizers, &subscribers, scope_stack.clone(), ); diff --git a/crates/core/tests/integration/middleware_tests.rs b/crates/core/tests/integration/middleware_tests.rs index 7e8763850..d5225bbfa 100644 --- a/crates/core/tests/integration/middleware_tests.rs +++ b/crates/core/tests/integration/middleware_tests.rs @@ -31,13 +31,14 @@ use nemo_relay::api::registry::{ deregister_llm_request_intercept, deregister_llm_sanitize_request_guardrail, deregister_llm_sanitize_response_guardrail, deregister_llm_stream_execution_intercept, deregister_mark_sanitize_guardrail, deregister_scope_sanitize_end_guardrail, - deregister_tool_conditional_execution_guardrail, deregister_tool_execution_intercept, - deregister_tool_request_intercept, deregister_tool_sanitize_request_guardrail, - deregister_tool_sanitize_response_guardrail, register_llm_conditional_execution_guardrail, - register_llm_execution_intercept, register_llm_request_intercept, - register_llm_sanitize_request_guardrail, register_llm_sanitize_response_guardrail, - register_llm_stream_execution_intercept, register_mark_sanitize_guardrail, - register_scope_sanitize_end_guardrail, register_tool_conditional_execution_guardrail, + deregister_scope_sanitize_start_guardrail, deregister_tool_conditional_execution_guardrail, + deregister_tool_execution_intercept, deregister_tool_request_intercept, + deregister_tool_sanitize_request_guardrail, deregister_tool_sanitize_response_guardrail, + register_llm_conditional_execution_guardrail, register_llm_execution_intercept, + register_llm_request_intercept, register_llm_sanitize_request_guardrail, + register_llm_sanitize_response_guardrail, register_llm_stream_execution_intercept, + register_mark_sanitize_guardrail, register_scope_sanitize_end_guardrail, + register_scope_sanitize_start_guardrail, register_tool_conditional_execution_guardrail, register_tool_execution_intercept, register_tool_request_intercept, register_tool_sanitize_request_guardrail, register_tool_sanitize_response_guardrail, scope_register_llm_conditional_execution_guardrail, scope_register_llm_execution_intercept, @@ -3350,6 +3351,86 @@ async fn test_llm_request_intercept_pending_marks_preserve_order_and_break_chain } } +#[tokio::test] +async fn test_managed_llm_event_sanitizers_run_off_execution_path_in_fifo_order() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let sanitizer_started = Arc::new(tokio::sync::Notify::new()); + let sanitizer_release = Arc::new(tokio::sync::Notify::new()); + register_scope_sanitize_start_guardrail( + "managed_async_publication_sanitizer", + 1, + Arc::new({ + let sanitizer_started = Arc::clone(&sanitizer_started); + let sanitizer_release = Arc::clone(&sanitizer_release); + move |_event, fields| { + let sanitizer_started = Arc::clone(&sanitizer_started); + let sanitizer_release = Arc::clone(&sanitizer_release); + Box::pin(async move { + sanitizer_started.notify_one(); + sanitizer_release.notified().await; + Ok(fields) + }) + } + }), + ) + .unwrap(); + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = Arc::clone(&events); + register_subscriber( + "managed_async_publication_observer", + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .unwrap(); + + let call = tokio::spawn(async { + llm_call_execute( + LlmCallExecuteParams::builder() + .name("managed-async-publication") + .request(LlmRequest { + headers: serde_json::Map::new(), + content: json!({"prompt": "hello"}), + }) + .func(Arc::new(|_| { + Box::pin(async { Ok(json!({"response": "done"})) }) + })) + .build(), + ) + .await + }); + tokio::time::timeout( + std::time::Duration::from_secs(2), + sanitizer_started.notified(), + ) + .await + .expect("managed start sanitizer did not run on the dispatcher"); + let result = tokio::time::timeout(std::time::Duration::from_secs(1), call) + .await + .expect("event sanitizer blocked managed provider execution") + .expect("managed call task should join") + .expect("managed call should succeed"); + assert_eq!(result, json!({"response": "done"})); + + sanitizer_release.notify_one(); + flush_subscribers().unwrap(); + let events = events.lock().unwrap(); + let lifecycle = events + .iter() + .filter(|event| event.name() == "managed-async-publication") + .map(|event| event.scope_category()) + .collect::>(); + assert_eq!( + lifecycle, + [Some(ScopeCategory::Start), Some(ScopeCategory::End),] + ); + drop(events); + + deregister_scope_sanitize_start_guardrail("managed_async_publication_sanitizer").unwrap(); + deregister_subscriber("managed_async_publication_observer").unwrap(); +} + #[tokio::test] async fn test_managed_llm_emits_pending_marks_under_started_scope() { let _lock = TEST_MUTEX.lock().unwrap(); diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index 63320c7a8..dbfd1bb14 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -831,6 +831,72 @@ async fn llm_worker_codec_capabilities_are_active_only_during_sanitizer_invocati ); } +#[tokio::test(flavor = "multi_thread")] +async fn cancelling_worker_sanitizer_expires_codec_capability() { + enable_operational_logs(); + let (started_tx, started_rx) = oneshot::channel(); + let started_tx = Arc::new(Mutex::new(Some(started_tx))); + let (callback, _shutdown, _cancel_rx) = fake_callback_service_with_handlers( + { + let started_tx = Arc::clone(&started_tx); + move |request| { + let started_tx = Arc::clone(&started_tx); + Box::pin(async move { + let invocation_id = request.invocation_id; + let Some(invoke_request_payload::Payload::Llm(invocation)) = request.payload + else { + panic!("LLM sanitizer must receive an LLM invocation"); + }; + let Some(llm_invocation::SanitizeContext::RequestSanitizeContext(context)) = + invocation.sanitize_context + else { + panic!("request sanitizer context must be present"); + }; + let capability_id = context + .codec_capability_id + .expect("request codec capability must be present"); + if let Some(started) = started_tx.lock().expect("started lock").take() { + let _ = started.send((capability_id, invocation_id)); + } + std::future::pending::().await + }) + } + }, + |_| Box::pin(tokio_stream::empty()), + ) + .await; + let callback_task = callback.clone(); + let task = tokio::spawn(async move { + callback_task + .invoke_llm_sanitize_request( + "cancel-codec", + valid_llm_request(), + LlmSanitizeRequestContext::for_request_codec(Some(Arc::new(OpenAIChatCodec))), + ) + .await + }); + let (capability_id, invocation_id) = + tokio::time::timeout(std::time::Duration::from_secs(1), started_rx) + .await + .expect("worker sanitizer should start") + .expect("worker sanitizer should publish its capability"); + callback + .host_state + .request_codec(&capability_id, &invocation_id) + .expect("capability must be active while the sanitizer is pending"); + + task.abort(); + let _ = task.await; + let error = match callback + .host_state + .request_codec(&capability_id, &invocation_id) + { + Ok(_) => panic!("cancelled sanitizer must expire its codec capability"), + Err(error) => error, + }; + assert_eq!(error.code(), tonic::Code::NotFound); +} + #[tokio::test(flavor = "multi_thread")] async fn callback_stream_transport_error_surfaces_to_host_stream() { enable_operational_logs(); diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index c2a256556..202eef880 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -298,6 +298,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), _callback_user_data: None, }); @@ -334,6 +335,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), _callback_user_data: None, }); @@ -359,6 +361,60 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { } } +#[test] +fn native_async_next_is_permanently_one_shot() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let next = Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::Tool({ + let calls = Arc::clone(&calls); + Arc::new(move |value| { + let calls = Arc::clone(&calls); + Box::pin(async move { + calls.fetch_add(1, Ordering::SeqCst); + Ok(value) + }) + }) + }), + runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + next_abort: Mutex::new(None), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let invocation = native_string_from_json(&json!({"value": 1})).unwrap(); + + assert_eq!( + unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, + NemoRelayStatus::Ok + ); + runtime.block_on(receiver).unwrap().unwrap(); + assert_eq!( + unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, + NemoRelayStatus::InvalidArg + ); + runtime.block_on(tokio::task::yield_now()); + assert_eq!(calls.load(Ordering::SeqCst), 1); + + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_completion_release(completion_ref); + } +} + #[test] fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlement() { let runtime = tokio::runtime::Builder::new_current_thread() @@ -369,6 +425,7 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), _callback_user_data: None, }); @@ -402,6 +459,7 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(true), + next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), _callback_user_data: None, }); @@ -456,6 +514,7 @@ fn cancelling_completion_aborts_pending_native_next() { let completion = Arc::new(NativeAsyncCompletion { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), _callback_user_data: None, }); diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 7e076832e..fa9e28bd0 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -74,6 +74,7 @@ use nemo_relay_adaptive::{AdaptiveConfig, AdaptiveRuntime as CoreAdaptiveRuntime use nemo_relay_pii_redaction::component::register_pii_redaction_component; use crate::callable; +use crate::callback_factory; use crate::convert::{ callback_json, clear_last_callback_error as clear_recorded_callback_error, get_last_callback_error as get_recorded_callback_error, opt_json, parse_timestamp_micros, @@ -83,6 +84,21 @@ use crate::promise_call::PromiseAwareFn; use crate::stream::LlmStream; use crate::types::{LlmHandle, ScopeHandle, ScopeStack, ScopeType, ToolHandle}; +fn effective_scope_stack(env: &Env) -> napi::Result { + Ok(callback_factory::callback_scope_stack(env)?.unwrap_or_else(current_scope_stack_handle)) +} + +fn with_effective_scope_stack(env: &Env, callback: impl FnOnce() -> T) -> napi::Result { + let scope_stack = effective_scope_stack(env)?; + Ok(with_scope_stack_handle(scope_stack, callback)) +} + +fn effective_scope_top( + scope_stack: &nemo_relay::api::runtime::ScopeStackHandle, +) -> nemo_relay::api::scope::ScopeHandle { + with_scope_stack_handle(scope_stack.clone(), task_scope_top) +} + #[napi::module_init] fn init() { initialize_shared_runtime_binding("node") @@ -1576,8 +1592,8 @@ pub fn create_scope_stack() -> ScopeStack { /// Capture the current Relay causal parent for application-managed transport. #[napi] -pub fn capture_propagation_context() -> napi::Result { - capture_propagation_context_handle() +pub fn capture_propagation_context(env: Env) -> napi::Result { + with_effective_scope_stack(&env, capture_propagation_context_handle)? .map(propagation_context_to_napi) .map_err(|error| napi::Error::from_reason(error.to_string())) } @@ -1585,6 +1601,7 @@ pub fn capture_propagation_context() -> napi::Result { /// Capture the current parent with an optional stable application session root. #[napi] pub fn capture_propagation_context_with_root( + env: Env, root_uuid: Option, ) -> napi::Result { let root_uuid = root_uuid @@ -1592,9 +1609,11 @@ pub fn capture_propagation_context_with_root( .map(uuid::Uuid::parse_str) .transpose() .map_err(|error| napi::Error::from_reason(format!("invalid root UUID: {error}")))?; - capture_propagation_context_with_root_handle(root_uuid) - .map(propagation_context_to_napi) - .map_err(|error| napi::Error::from_reason(error.to_string())) + with_effective_scope_stack(&env, || { + capture_propagation_context_with_root_handle(root_uuid) + })? + .map(propagation_context_to_napi) + .map_err(|error| napi::Error::from_reason(error.to_string())) } /// Serialize a Relay causal context to the JSON wire format. @@ -1636,10 +1655,10 @@ pub fn with_scope_stack(stack: &ScopeStack, callback: JsFunction) -> napi::Resul /// Returns the current execution context's scope stack handle. #[napi] -pub fn current_scope_stack() -> ScopeStack { - ScopeStack { - inner: current_scope_stack_handle(), - } +pub fn current_scope_stack(env: Env) -> napi::Result { + Ok(ScopeStack { + inner: effective_scope_stack(&env)?, + }) } /// Binds a scope stack to the current thread. @@ -1655,8 +1674,11 @@ pub fn set_thread_scope_stack(stack: &ScopeStack) { /// thread, or the caller is inside a task-local scope. Returns `false` when /// only the auto-created default is present. #[napi] -pub fn scope_stack_active() -> bool { - scope_stack_is_active() +pub fn scope_stack_active(env: Env) -> napi::Result { + if callback_factory::callback_scope_stack(&env)?.is_some() { + return Ok(true); + } + Ok(scope_stack_is_active()) } /// Returns the most recent callback error that could not be surfaced through a direct exception. @@ -1773,8 +1795,8 @@ pub fn test_closed_promise_aware_call(env: Env, func: JsFunction) -> Result Result { - core_scope_api::get_handle() +pub fn get_handle(env: Env) -> Result { + with_effective_scope_stack(&env, core_scope_api::get_handle)? .map(ScopeHandle::from) .map_err(to_napi_err) } @@ -1794,6 +1816,7 @@ pub fn get_handle() -> Result { #[napi] #[allow(clippy::too_many_arguments)] pub fn push_scope( + env: Env, name: String, scope_type: ScopeType, handle: Option<&ScopeHandle>, @@ -1805,18 +1828,20 @@ pub fn push_scope( ) -> Result { let attrs = ScopeAttributes::from_bits_truncate(attributes.unwrap_or(0)); let timestamp = parse_timestamp_micros(timestamp)?; - core_scope_api::push_scope( - core_scope_api::PushScopeParams::builder() - .name(name.as_str()) - .scope_type(scope_type.into()) - .parent_opt(handle.map(|h| &h.inner)) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .input_opt(opt_json(input)) - .timestamp_opt(timestamp) - .build(), - ) + with_effective_scope_stack(&env, || { + core_scope_api::push_scope( + core_scope_api::PushScopeParams::builder() + .name(name.as_str()) + .scope_type(scope_type.into()) + .parent_opt(handle.map(|h| &h.inner)) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .input_opt(opt_json(input)) + .timestamp_opt(timestamp) + .build(), + ) + })? .map(ScopeHandle::from) .map_err(to_napi_err) } @@ -1831,20 +1856,23 @@ pub fn push_scope( /// Throws if the handle does not match the current top scope. #[napi] pub fn pop_scope( + env: Env, handle: &ScopeHandle, output: Option, timestamp: Option, metadata: Option, ) -> Result<()> { let timestamp = parse_timestamp_micros(timestamp)?; - core_scope_api::pop_scope( - core_scope_api::PopScopeParams::builder() - .handle_uuid(&handle.inner.uuid) - .output_opt(opt_json(output)) - .timestamp_opt(timestamp) - .metadata_opt(opt_json(metadata)) - .build(), - ) + with_effective_scope_stack(&env, || { + core_scope_api::pop_scope( + core_scope_api::PopScopeParams::builder() + .handle_uuid(&handle.inner.uuid) + .output_opt(opt_json(output)) + .timestamp_opt(timestamp) + .metadata_opt(opt_json(metadata)) + .build(), + ) + })? .map_err(to_napi_err)?; Ok(()) } @@ -1876,21 +1904,23 @@ pub fn with_scope( input: Option, ) -> Result { let attrs = ScopeAttributes::from_bits_truncate(attributes.unwrap_or(0)); - let scope_handle = core_scope_api::push_scope( - core_scope_api::PushScopeParams::builder() - .name(name.as_str()) - .scope_type(scope_type.into()) - .parent_opt(handle.map(|h| &h.inner)) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .input_opt(opt_json(input)) - .build(), - ) + let scope_stack = effective_scope_stack(&env)?; + let scope_handle = with_scope_stack_handle(scope_stack.clone(), || { + core_scope_api::push_scope( + core_scope_api::PushScopeParams::builder() + .name(name.as_str()) + .scope_type(scope_type.into()) + .parent_opt(handle.map(|h| &h.inner)) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .input_opt(opt_json(input)) + .build(), + ) + }) .map(ScopeHandle::from) .map_err(to_napi_err)?; - let scope_stack = current_scope_stack_handle(); let scope_uuid = scope_handle.inner.uuid; // Hand the callback a real `ScopeHandle` instance, matching the Rust, // Python bindings, so it can be passed back into `event`, @@ -1903,15 +1933,17 @@ pub fn with_scope( let pa_fn = std::sync::Arc::new( crate::promise_call::PromiseAwareFn::new(&env, &callback).map_err(|e| { let status_message = format!("failed to create PromiseAwareFn: {e}"); - let _ = core_scope_api::pop_scope( - core_scope_api::PopScopeParams::builder() - .handle_uuid(&scope_uuid) - .metadata_opt(Some(otel_status_metadata( - "ERROR", - Some(status_message.clone()), - ))) - .build(), - ); + let _ = with_scope_stack_handle(scope_stack.clone(), || { + core_scope_api::pop_scope( + core_scope_api::PopScopeParams::builder() + .handle_uuid(&scope_uuid) + .metadata_opt(Some(otel_status_metadata( + "ERROR", + Some(status_message.clone()), + ))) + .build(), + ) + }); napi::Error::from_reason(status_message) })?, ); @@ -1959,6 +1991,7 @@ pub fn with_scope( /// It must be a safe integer number; omit it to use the current runtime time. #[napi] pub fn event( + env: Env, name: String, handle: Option<&ScopeHandle>, data: Option, @@ -1966,15 +1999,17 @@ pub fn event( timestamp: Option, ) -> Result<()> { let timestamp = parse_timestamp_micros(timestamp)?; - core_scope_api::event( - core_scope_api::EmitMarkEventParams::builder() - .name(&name) - .parent_opt(handle.map(|h| &h.inner)) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .timestamp_opt(timestamp) - .build(), - ) + with_effective_scope_stack(&env, || { + core_scope_api::event( + core_scope_api::EmitMarkEventParams::builder() + .name(&name) + .parent_opt(handle.map(|h| &h.inner)) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .timestamp_opt(timestamp) + .build(), + ) + })? .map_err(to_napi_err) } @@ -1996,6 +2031,7 @@ pub fn event( #[napi] #[allow(clippy::too_many_arguments)] pub fn tool_call( + env: Env, name: String, args: Json, handle: Option<&ScopeHandle>, @@ -2007,18 +2043,20 @@ pub fn tool_call( ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); let timestamp = parse_timestamp_micros(timestamp)?; - core_tool_api::tool_call( - core_tool_api::ToolCallParams::builder() - .name(name.as_str()) - .args(args) - .parent_opt(handle.map(|h| &h.inner)) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .tool_call_id_opt(tool_call_id) - .timestamp_opt(timestamp) - .build(), - ) + with_effective_scope_stack(&env, || { + core_tool_api::tool_call( + core_tool_api::ToolCallParams::builder() + .name(name.as_str()) + .args(args) + .parent_opt(handle.map(|h| &h.inner)) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .tool_call_id_opt(tool_call_id) + .timestamp_opt(timestamp) + .build(), + ) + })? .map(ToolHandle::from) .map_err(to_napi_err) } @@ -2033,6 +2071,7 @@ pub fn tool_call( /// It must be a safe integer number; omit it to use the runtime default end timestamp. #[napi] pub fn tool_call_end( + env: Env, handle: &ToolHandle, result: Json, data: Option, @@ -2040,15 +2079,17 @@ pub fn tool_call_end( timestamp: Option, ) -> Result<()> { let timestamp = parse_timestamp_micros(timestamp)?; - core_tool_api::tool_call_end( - core_tool_api::ToolCallEndParams::builder() - .handle(&handle.inner) - .result(result) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .timestamp_opt(timestamp) - .build(), - ) + with_effective_scope_stack(&env, || { + core_tool_api::tool_call_end( + core_tool_api::ToolCallEndParams::builder() + .handle(&handle.inner) + .result(result) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .timestamp_opt(timestamp) + .build(), + ) + })? .map_err(to_napi_err) } @@ -2073,13 +2114,13 @@ pub fn tool_call_execute( metadata: Option, ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) - .unwrap_or_else(task_scope_top); + .unwrap_or_else(|| effective_scope_top(&scope_stack)); let callback = callable::safe_execution_callback(&env, &func)?; let exec_fn = callable::wrap_js_tool_exec_fn(json_callback_tsfn(&env, &callback)?); let default_fn: ToolExecutionNextFn = std::sync::Arc::new(move |args| exec_fn(args)); - let scope_stack = current_scope_stack_handle(); env.execute_tokio_future( async move { @@ -2126,10 +2167,10 @@ pub fn tool_call_execute_async( metadata: Option, ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) - .unwrap_or_else(task_scope_top); - let scope_stack = current_scope_stack_handle(); + .unwrap_or_else(|| effective_scope_top(&scope_stack)); // Create promise-aware wrapper — this must happen on the JS thread (we have Env). let pa_fn = std::sync::Arc::new( @@ -2186,6 +2227,7 @@ pub fn tool_call_execute_async( #[allow(clippy::too_many_arguments)] #[napi] pub fn llm_call( + env: Env, name: String, request: Json, handle: Option<&ScopeHandle>, @@ -2209,7 +2251,7 @@ pub fn llm_call( .model_name_opt(model_name) .timestamp_opt(timestamp) .build(); - core_llm_api::llm_call(params) + with_effective_scope_stack(&env, || core_llm_api::llm_call(params))? .map(LlmHandle::from) .map_err(to_napi_err) } @@ -2224,6 +2266,7 @@ pub fn llm_call( /// It must be a safe integer number; omit it to use the runtime default end timestamp. #[napi] pub fn llm_call_end( + env: Env, handle: &LlmHandle, response: Json, data: Option, @@ -2231,15 +2274,17 @@ pub fn llm_call_end( timestamp: Option, ) -> Result<()> { let timestamp = parse_timestamp_micros(timestamp)?; - core_llm_api::llm_call_end( - core_llm_api::LlmCallEndParams::builder() - .handle(&handle.inner) - .response(response) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .timestamp_opt(timestamp) - .build(), - ) + with_effective_scope_stack(&env, || { + core_llm_api::llm_call_end( + core_llm_api::LlmCallEndParams::builder() + .handle(&handle.inner) + .response(response) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .timestamp_opt(timestamp) + .build(), + ) + })? .map_err(to_napi_err) } @@ -2270,9 +2315,10 @@ pub fn llm_call_execute( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) - .unwrap_or_else(task_scope_top); + .unwrap_or_else(|| effective_scope_top(&scope_stack)); let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; let callback = callable::safe_execution_callback(&env, &func)?; @@ -2300,8 +2346,6 @@ pub fn llm_call_execute( codec_references.extend(references); codec }); - let scope_stack = current_scope_stack_handle(); - env.execute_tokio_future( async move { TASK_SCOPE_STACK @@ -2352,13 +2396,12 @@ pub fn llm_call_execute_async( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) - .unwrap_or_else(task_scope_top); + .unwrap_or_else(|| effective_scope_top(&scope_stack)); let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; - let scope_stack = current_scope_stack_handle(); - let pa_fn = std::sync::Arc::new( crate::promise_call::PromiseAwareFn::new(&env, &func).map_err(|e| { napi::Error::from_reason(format!("failed to create PromiseAwareFn: {e}")) @@ -2459,9 +2502,10 @@ pub fn llm_stream_call_execute( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) - .unwrap_or_else(task_scope_top); + .unwrap_or_else(|| effective_scope_top(&scope_stack)); let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; @@ -2532,8 +2576,6 @@ pub fn llm_stream_call_execute( codec }); let completion_codec_references = codec_references.clone(); - let scope_stack = current_scope_stack_handle(); - env.execute_tokio_future( async move { TASK_SCOPE_STACK @@ -3723,7 +3765,7 @@ pub fn scope_deregister_subscriber(scope_uuid: String, name: String) -> Result Result { - let scope_stack = current_scope_stack_handle(); + let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { TASK_SCOPE_STACK @@ -3742,7 +3784,7 @@ pub fn tool_request_intercepts(env: Env, name: String, args: Json) -> Result Result { - let scope_stack = current_scope_stack_handle(); + let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { TASK_SCOPE_STACK @@ -3766,7 +3808,7 @@ pub fn tool_conditional_execution(env: Env, name: String, args: Json) -> Result< pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result { let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; - let scope_stack = current_scope_stack_handle(); + let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { TASK_SCOPE_STACK @@ -3796,7 +3838,7 @@ pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result Result { let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; - let scope_stack = current_scope_stack_handle(); + let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { TASK_SCOPE_STACK diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index 24e250d49..92a84d894 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -3,7 +3,11 @@ //! Cached JavaScript callback wrapper factories for the Node binding. -use napi::{Env, JsFunction, JsObject, JsUnknown, NapiRaw, NapiValue}; +use napi::bindgen_prelude::FromNapiValue; +use napi::{Env, JsFunction, JsObject, JsUnknown, NapiRaw, NapiValue, ValueType}; +use nemo_relay::api::runtime::ScopeStackHandle; + +use crate::types::ScopeStack; const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v2"; @@ -52,8 +56,8 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { return result; } - function callPromise(fn, arg0, spread, next, resolve, reject, publication) { - const token = { active: publication }; + function callPromise(fn, arg0, spread, next, resolve, reject, publication, scopeStack) { + const token = { active: publication, scopeStack }; const invoke = () => { Promise.resolve().then(() => ( next === undefined @@ -61,9 +65,11 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { : (spread ? fn(...arg0, next) : fn(arg0, next)) )).then((value) => jsonValue(value === undefined ? null : value)).then((value) => { token.active = false; + token.scopeStack = null; resolve(value); }, (error) => { token.active = false; + token.scopeStack = null; let message = 'unknown error'; try { if (typeof error === 'string') { @@ -97,7 +103,7 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { }, promise(fn) { - return function __nemo_relay_promise_wrapper(error, arg0, spread, next, resolve, reject, publication) { + return function __nemo_relay_promise_wrapper(error, arg0, spread, next, resolve, reject, publication, scopeStack) { if (error != null) { let message = 'unknown error'; try { @@ -106,13 +112,17 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { reject(message); return; } - callPromise(fn, arg0, spread, next, resolve, reject, publication); + callPromise(fn, arg0, spread, next, resolve, reject, publication, scopeStack); }; }, eventSanitizerCallbackActive() { return eventSanitizerContext.getStore()?.active === true; }, + + callbackScopeStack() { + return eventSanitizerContext.getStore()?.scopeStack; + }, }; })()"#; @@ -168,3 +178,14 @@ pub(crate) fn event_sanitizer_callback_active(env: &Env) -> napi::Result { .coerce_to_bool()? .get_value() } + +pub(crate) fn callback_scope_stack(env: &Env) -> napi::Result> { + let factories = callback_factories(env)?; + let callback: JsFunction = factories.get_named_property("callbackScopeStack")?; + let value = callback.call::(None, &[])?; + if matches!(value.get_type()?, ValueType::Undefined | ValueType::Null) { + return Ok(None); + } + let stack = unsafe { <&ScopeStack as FromNapiValue>::from_napi_value(env.raw(), value.raw())? }; + Ok(Some(stack.inner.clone())) +} diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index 9a4249900..5bab8776d 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -20,9 +20,11 @@ use napi::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}; use napi::{Env, JsFunction, JsUnknown, NapiRaw, NapiValue}; use serde_json::Value as Json; +use nemo_relay::api::runtime::{ScopeStackHandle, current_scope_stack}; use nemo_relay::error::{FlowError, Result as FlowResult}; use crate::callback_factory; +use crate::types::ScopeStack; pub type JsonNextFn = Arc Pin> + Send>> + Send + Sync>; @@ -56,6 +58,8 @@ struct CallArgs { spread: bool, next: Option, publication: bool, + /// Scope stack installed by Relay while queued publication middleware runs. + scope_stack: Option, completion: CallCompletion, } @@ -234,7 +238,22 @@ impl PromiseAwareFn { ctx.env.get_boolean(ctx.value.publication)?.raw(), ) }; - let args = vec![arg0, spread, next, resolve, reject, publication]; + let scope_stack = match ctx.value.scope_stack { + Some(scope_stack) => { + let scope_stack = ScopeStack::from(scope_stack).into_instance(ctx.env)?; + unsafe { JsUnknown::from_raw_unchecked(ctx.env.raw(), scope_stack.raw()) } + } + None => undefined_to_unknown(&ctx.env)?, + }; + let args = vec![ + arg0, + spread, + next, + resolve, + reject, + publication, + scope_stack, + ]; Ok(args) })?; @@ -355,6 +374,7 @@ impl PromiseAwareFn { spread: mode.spread, next, publication: mode.publication, + scope_stack: mode.publication.then(current_scope_stack), completion: CallCompletion::new(sender), }), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 7be7f1ff5..82c48847e 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -129,6 +129,48 @@ describe('event sanitizer registries', () => { assert.deepEqual(events.at(-1).data, { sanitized: true }); }); + it('preserves the emitting scope stack across queued sanitizer awaits', async () => { + const events = capture('node-event-sanitize-scope-context-sub'); + const originalStack = lib.currentScopeStack(); + const emitterStack = lib.createScopeStack(); + const unrelatedStack = lib.createScopeStack(); + let emitterScopeUuid; + const observedParents = []; + + lib.registerMarkSanitizeGuardrail('node-event-scope-context', 0, async (event, fields) => { + if (event.name !== 'scope-context-original') { + return fields; + } + observedParents.push(lib.getHandle().uuid); + await new Promise((resolve) => setImmediate(resolve)); + observedParents.push(lib.getHandle().uuid); + lib.event('scope-context-nested', null, { originalParent: event.parent_uuid }); + return fields; + }); + + try { + lib.withScopeStack(emitterStack, () => { + emitterScopeUuid = lib.pushScope('scope-context-emitter', lib.ScopeType.Agent).uuid; + lib.event('scope-context-original', null, {}); + }); + lib.setThreadScopeStack(unrelatedStack); + const unrelatedRootUuid = lib.getHandle().uuid; + + await lib.flushSubscribers(); + await lib.flushSubscribers(); + await waitFor(events, 2); + + assert.deepEqual(observedParents, [emitterScopeUuid, emitterScopeUuid]); + const nested = events.find((event) => event.name === 'scope-context-nested'); + assert.equal(nested.parent_uuid, emitterScopeUuid); + assert.notEqual(nested.parent_uuid, unrelatedRootUuid); + } finally { + lib.setThreadScopeStack(originalStack); + lib.deregisterMarkSanitizeGuardrail('node-event-scope-context'); + lib.deregisterSubscriber('node-event-sanitize-scope-context-sub'); + } + }); + it('preserves snapshotted sanitizers after deregistration', async () => { const events = capture('node-event-sanitize-snapshot-sub'); let blockerEntered; @@ -216,7 +258,7 @@ describe('event sanitizer registries', () => { } }); - it('treats inline managed sanitizers as real flush barriers', async () => { + it('queues managed event sanitizers without blocking execution', async () => { lib.registerSubscriber('node-event-inline-flush-sub', () => {}); let blockerEntered; const entered = new Promise((resolve) => { @@ -243,12 +285,15 @@ describe('event sanitizer registries', () => { lib.event('inline-flush-blocker', null, { raw: true }); await entered; const execution = lib.toolCallExecute('inline-flush-tool', {}, (args) => args); - await new Promise((resolve) => setTimeout(resolve, 50)); + const executionState = await Promise.race([ + execution.then(() => 'executed'), + new Promise((resolve) => setTimeout(() => resolve('blocked'), 250)), + ]); + assert.equal(executionState, 'executed'); assert.equal(inlineFlushReturned, false); releaseBlocker(); - await execution; - assert.equal(inlineFlushReturned, true); await lib.flushSubscribers(); + assert.equal(inlineFlushReturned, true); } finally { releaseBlocker(); lib.deregisterMarkSanitizeGuardrail('node-event-inline-flush-blocker'); diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 96d58e15d..1f851f4bf 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -205,12 +205,23 @@ fn capture_python_task_locals() -> Option { Python::attach(|py| pyo3_async_runtimes::tokio::get_current_locals(py).ok()) } -struct PythonPublicationContext(TaskLocals); +struct PythonPublicationContext { + task_locals: Option, + context: Py, +} pub(crate) fn capture_python_publication_context() -> Option { - capture_python_task_locals() - .map(PythonPublicationContext) - .map(|context| Arc::new(context) as PublicationContext) + Python::attach(|py| { + let context = py + .import("contextvars") + .and_then(|module| module.call_method0("copy_context")) + .ok()? + .unbind(); + Some(Arc::new(PythonPublicationContext { + task_locals: pyo3_async_runtimes::tokio::get_current_locals(py).ok(), + context, + }) as PublicationContext) + }) } fn running_task_locals(locals: TaskLocals) -> Option { @@ -231,7 +242,8 @@ fn running_task_locals(locals: TaskLocals) -> Option { fn task_locals_with_running_loop(registered: Option<&TaskLocals>) -> Option { publication_context::() - .and_then(|context| running_task_locals(context.0.clone())) + .and_then(|context| context.task_locals.clone()) + .and_then(running_task_locals) .or_else(|| capture_python_task_locals().and_then(running_task_locals)) .or_else(|| registered.cloned().and_then(running_task_locals)) } @@ -530,17 +542,31 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { Arc::new(move |name: String, args: Json| { let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); + let publication_context = publication_context::(); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let py_args = json_to_py(py, &args) .map_err(|e| FlowError::Internal(format!("tool json_to_py failed: {e}")))?; - let result = if publication { - py.import("nemo_relay._event_sanitizer_context") + let result = match (publication_context.as_ref(), publication) { + (Some(context), true) => py + .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) - .and_then(|invoke| invoke.call1((py_fn.bind(py), name, py_args))) - } else { - py_fn.bind(py).call1((name, py_args)) + .and_then(|invoke| { + context + .context + .bind(py) + .call_method1("run", (invoke, py_fn.bind(py), name, py_args)) + }), + (Some(context), false) => context + .context + .bind(py) + .call_method1("run", (py_fn.bind(py), name, py_args)), + (None, true) => py + .import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("invoke")) + .and_then(|invoke| invoke.call1((py_fn.bind(py), name, py_args))), + (None, false) => py_fn.bind(py).call1((name, py_args)), } .map_err(|e| FlowError::Internal(format!("Python tool callback failed: {e}")))?; split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) @@ -952,6 +978,7 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest move |request: LlmRequest, context: LlmSanitizeRequestContext| { let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); + let publication_context = publication_context::(); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { @@ -960,12 +987,25 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest PyLLMRequest { inner: request }, PyLlmSanitizeRequestContext { inner: context }, ); - let result = if publication { - py.import("nemo_relay._event_sanitizer_context") + let result = match (publication_context.as_ref(), publication) { + (Some(context), true) => py + .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) - .and_then(|invoke| invoke.call1((py_fn.bind(py), args.0, args.1))) - } else { - py_fn.bind(py).call1(args) + .and_then(|invoke| { + context + .context + .bind(py) + .call_method1("run", (invoke, py_fn.bind(py), args.0, args.1)) + }), + (Some(context), false) => context + .context + .bind(py) + .call_method1("run", (py_fn.bind(py), args.0, args.1)), + (None, true) => py + .import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("invoke")) + .and_then(|invoke| invoke.call1((py_fn.bind(py), args.0, args.1))), + (None, false) => py_fn.bind(py).call1(args), } .map_err(|e| FlowError::Internal(e.to_string()))?; split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) @@ -1177,18 +1217,32 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon Arc::new(move |response: Json, context: LlmSanitizeResponseContext| { let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); + let publication_context = publication_context::(); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let py_context = PyLlmSanitizeResponseContext { inner: context }; let py_response = json_to_py(py, &response) .map_err(|error| FlowError::Internal(error.to_string()))?; - let result = if publication { - py.import("nemo_relay._event_sanitizer_context") + let result = match (publication_context.as_ref(), publication) { + (Some(context), true) => py + .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) - .and_then(|invoke| invoke.call1((py_fn.bind(py), py_response, py_context))) - } else { - py_fn.bind(py).call1((py_response, py_context)) + .and_then(|invoke| { + context.context.bind(py).call_method1( + "run", + (invoke, py_fn.bind(py), py_response, py_context), + ) + }), + (Some(context), false) => context + .context + .bind(py) + .call_method1("run", (py_fn.bind(py), py_response, py_context)), + (None, true) => py + .import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("invoke")) + .and_then(|invoke| invoke.call1((py_fn.bind(py), py_response, py_context))), + (None, false) => py_fn.bind(py).call1((py_response, py_context)), } .map_err(|error| FlowError::Internal(error.to_string()))?; split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) @@ -1251,6 +1305,7 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { Arc::new(move |event: Arc, fields: EventSanitizeFields| { let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); + let publication_context = publication_context::(); Box::pin(async move { let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { @@ -1292,9 +1347,14 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .map_err(|error| FlowError::Internal(error.to_string()))?; - let result = invoke - .call1((py_fn.bind(py), py_event, py_fields)) - .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match publication_context.as_ref() { + Some(context) => context + .context + .bind(py) + .call_method1("run", (invoke, py_fn.bind(py), py_event, py_fields)), + None => invoke.call1((py_fn.bind(py), py_event, py_fields)), + } + .map_err(|error| FlowError::Internal(error.to_string()))?; split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) }, ); diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index 377bee552..3e8e054e4 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -51,11 +51,90 @@ def log_event(event): if TYPE_CHECKING: from nemo_relay import Event + +def _finish_flush(completed: asyncio.Future[None], error: BaseException | None) -> None: + if completed.done(): + return + if error is None: + completed.set_result(None) + else: + completed.set_exception(error) + + +class _FlushBridge: + """Coalesce asynchronous flush barriers onto one native-wait thread.""" + + def __init__(self) -> None: + self._lock = threading.Lock() + self._wake = threading.Event() + self._pending: dict[int, tuple[asyncio.AbstractEventLoop, asyncio.Future[None]]] = {} + self._next_token = 0 + self._thread: threading.Thread | None = None + + def submit( + self, + loop: asyncio.AbstractEventLoop, + completed: asyncio.Future[None], + ) -> None: + thread_to_start: threading.Thread | None = None + with self._lock: + token = self._next_token + self._next_token += 1 + self._pending[token] = (loop, completed) + if self._thread is None: + self._thread = threading.Thread( + target=self._run, + name="nemo-relay-flush", + daemon=True, + ) + thread_to_start = self._thread + self._wake.set() + completed.add_done_callback(lambda _future: self._discard(token)) + if thread_to_start is not None: + thread_to_start.start() + + def _discard(self, token: int) -> None: + with self._lock: + self._pending.pop(token, None) + + def _run(self) -> None: + while True: + self._wake.wait() + with self._lock: + batch = list(self._pending.values()) + self._pending.clear() + self._wake.clear() + if not batch: + continue + try: + _native_flush() + except BaseException as error: + result = error + else: + result = None + for loop, completed in batch: + try: + loop.call_soon_threadsafe(_finish_flush, completed, result) + except RuntimeError: + pass + + +_flush_bridge = _FlushBridge() + + +def _after_fork_child() -> None: + global _flush_bridge + _native_after_fork_child() + # Never inspect the inherited bridge: its lock may have been held by a + # thread that does not exist in the child. + _flush_bridge = _FlushBridge() + + if hasattr(os, "register_at_fork"): os.register_at_fork( before=_native_before_fork, after_in_parent=_native_after_fork_parent, - after_in_child=_native_after_fork_child, + after_in_child=_after_fork_child, ) @@ -139,40 +218,15 @@ def flush() -> None: async def flush_async() -> None: """Wait asynchronously for subscriber callbacks already queued by Relay. - Use this barrier from an ``asyncio`` task. A daemon bridge thread waits for - the native dispatcher without blocking the Python event loop or process - shutdown when this coroutine is cancelled. + Use this barrier from an ``asyncio`` task. A process-local daemon bridge + thread coalesces concurrent barriers and waits for the native dispatcher + without blocking the Python event loop. """ if _event_sanitizer_callback_active(): return None loop = asyncio.get_running_loop() completed: asyncio.Future[None] = loop.create_future() - - def finish(error: BaseException | None) -> None: - if completed.done(): - return - if error is None: - completed.set_result(None) - else: - completed.set_exception(error) - - def wait_for_dispatcher() -> None: - try: - _native_flush() - except BaseException as error: - result = error - else: - result = None - try: - loop.call_soon_threadsafe(finish, result) - except RuntimeError: - pass - - threading.Thread( - target=wait_for_dispatcher, - name="nemo-relay-flush", - daemon=True, - ).start() + _flush_bridge.submit(loop, completed) await completed diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 652550de5..23e5bc520 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -125,6 +125,28 @@ async def emit(name: str) -> None: assert observed == {"request-a": "request-a", "request-b": "request-b"} +def test_sync_mark_sanitizer_uses_emitter_context(capture_events): + request_id = contextvars.ContextVar("request_id", default="registration") + observed: list[str] = [] + + def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + observed.append(request_id.get()) + return fields + + guardrails.register_mark_sanitize("python-sync-emitter-context", 0, sanitize) + try: + token = request_id.set("emission") + try: + scope.event("sync-emitter-context") + finally: + request_id.reset(token) + subscribers.flush() + finally: + guardrails.deregister_mark_sanitize("python-sync-emitter-context") + + assert observed == ["emission"] + + async def test_async_flush_keeps_originating_sanitizer_loop_running(capture_events): _capture_name, events = capture_events diff --git a/python/tests/test_subscribers.py b/python/tests/test_subscribers.py index 0b77e2980..fb90f0723 100644 --- a/python/tests/test_subscribers.py +++ b/python/tests/test_subscribers.py @@ -103,15 +103,26 @@ def test_deregister_nonexistent(self): assert not subscribers.deregister("nonexistent_sub") @pytest.mark.skipif(not hasattr(os, "fork"), reason="requires os.fork") - def test_async_flush_remains_usable_after_fork(self): + def test_fork_does_not_wait_for_pending_async_sanitizer(self): script = textwrap.dedent( """ import asyncio import os + import threading - from nemo_relay import subscribers + from nemo_relay import guardrails, scope, subscribers + + entered = threading.Event() + + async def stuck(_event, fields): + entered.set() + await asyncio.Event().wait() + return fields - asyncio.run(subscribers.flush_async()) + subscribers.register("fork-pending-sink", lambda _event: None) + guardrails.register_mark_sanitize("fork-pending-sanitizer", 0, stuck) + scope.event("fork-pending") + assert entered.wait(2) child = os.fork() if child == 0: async def flush(): @@ -124,7 +135,54 @@ async def flush(): os._exit(0) _, status = os.waitpid(child, 0) - raise SystemExit(os.waitstatus_to_exitcode(status)) + os._exit(os.waitstatus_to_exitcode(status)) + """ + ) + completed = subprocess.run( + [sys.executable, "-c", script], + check=False, + capture_output=True, + text=True, + timeout=5, + ) + assert completed.returncode == 0, completed.stderr + + def test_concurrent_cancelled_async_flushes_share_one_bridge_thread(self): + script = textwrap.dedent( + """ + import asyncio + import threading + + from nemo_relay import subscribers + + entered = threading.Event() + release = threading.Event() + + def blocked_flush(): + entered.set() + release.wait(2) + + subscribers._native_flush = blocked_flush + + async def run(): + tasks = [ + asyncio.create_task(subscribers.flush_async()) + for _ in range(100) + ] + assert await asyncio.to_thread(entered.wait, 1) + await asyncio.sleep(0.05) + bridge_threads = [ + thread + for thread in threading.enumerate() + if thread.name == "nemo-relay-flush" + ] + assert len(bridge_threads) == 1 + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + release.set() + + asyncio.run(run()) """ ) completed = subprocess.run( From 8ee99e4cf42ff6401390efab595cc012977a7c43 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 02:48:08 -0400 Subject: [PATCH 44/83] fix: preserve async middleware lifecycle context Signed-off-by: Will Killian --- crates/core/src/api/llm.rs | 4 +- .../src/api/runtime/subscriber_dispatcher.rs | 116 ++++++++++++++++++ crates/core/src/stream.rs | 33 ++--- crates/core/tests/unit/llm_api_tests.rs | 39 +++++- crates/node/src/api/mod.rs | 15 ++- crates/node/src/callable.rs | 5 +- crates/node/src/callback_factory.rs | 44 +++++++ crates/node/src/promise_call.rs | 4 +- crates/node/tests/event_sanitizers_tests.mjs | 10 ++ crates/node/tests/tools_tests.mjs | 27 ++++ python/nemo_relay/llm.py | 14 ++- python/nemo_relay/tools.py | 10 +- python/tests/test_llm.py | 38 ++++++ 13 files changed, 318 insertions(+), 41 deletions(-) diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index 3b36b6aa3..b6d68fcb6 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -591,9 +591,7 @@ fn enqueue_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriber let scope_stack = handle.captured_scope_stack().clone(); for (contribution, recorded_at) in contributions { let event = optimization_mark_event(handle, &contribution, recorded_at); - let Some(sanitizers) = snapshot_event_sanitizers(&event, &scope_stack) else { - break; - }; + let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); if dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack.clone()) { handle.optimization_recorder.mark_emitted(1); } else { diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 760d8d844..f68e95956 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -46,6 +46,12 @@ fn current_publication_context() -> Option { .or_else(|| THREAD_PUBLICATION_CONTEXT.with(|current| current.borrow().clone())) } +/// Capture the current opaque binding publication context for a spawned task. +#[doc(hidden)] +pub fn capture_publication_context() -> Option { + current_publication_context() +} + /// Run synchronous event emission with an opaque binding context snapshot. #[doc(hidden)] pub fn with_publication_context( @@ -111,12 +117,18 @@ mod native { type DispatcherState = Option, String>>; type SanitizerRuntimeState = Option>; + type BackgroundPublication = Pin + Send + 'static>>; + type BackgroundPublicationState = Option< + std::result::Result, String>, + >; struct ProcessState { dispatcher: Mutex, sanitizer_runtime: Mutex, + background_publications: Mutex, dispatcher_failure_logged: AtomicBool, sanitizer_runtime_failure_logged: AtomicBool, + background_publication_failure_logged: AtomicBool, } impl ProcessState { @@ -124,8 +136,10 @@ mod native { Self { dispatcher: Mutex::new(None), sanitizer_runtime: Mutex::new(None), + background_publications: Mutex::new(None), dispatcher_failure_logged: AtomicBool::new(false), sanitizer_runtime_failure_logged: AtomicBool::new(false), + background_publication_failure_logged: AtomicBool::new(false), } } } @@ -203,6 +217,66 @@ mod native { .map_err(|error| error.to_string()) } + fn start_background_publication_executor() + -> std::result::Result, String> { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| error.to_string())?; + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); + std::thread::Builder::new() + .name("nemo-relay-background-publication".into()) + .spawn(move || { + runtime.block_on(async move { + while let Some(publication) = receiver.recv().await { + tokio::spawn(publication); + } + }); + }) + .map_err(|error| error.to_string())?; + Ok(sender) + } + + pub(super) fn spawn_background_publication(future: F) -> bool + where + F: Future + Send + 'static, + { + let state = process_state(); + let sender = { + let mut executor = state + .background_publications + .lock() + .unwrap_or_else(|error| error.into_inner()); + executor + .get_or_insert_with(start_background_publication_executor) + .clone() + }; + match sender { + Ok(sender) if sender.send(Box::pin(future)).is_ok() => true, + Ok(_) => { + log::error!( + target: "nemo_relay.runtime", + event = "background_publication_executor_stopped"; + "Background publication executor stopped before accepting stream finalization" + ); + false + } + Err(error) + if !state + .background_publication_failure_logged + .swap(true, Ordering::AcqRel) => + { + log::error!( + target: "nemo_relay.runtime", + event = "background_publication_executor_failed"; + "Background publication executor failed to start: {error}" + ); + false + } + Err(_) => false, + } + } + #[cfg(test)] pub(super) fn dispatch_event(event: &Event, subscribers: &[EventSubscriberFn]) -> bool { if subscribers.is_empty() { @@ -735,6 +809,39 @@ mod native { "a delivery queued after a flush must not delay that flush" ); } + + #[test] + fn detached_publications_share_one_background_executor_thread() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + let (started_tx, started_rx) = mpsc::channel(); + let (release_tx, release_rx) = tokio::sync::watch::channel(false); + for _ in 0..32 { + let started_tx = started_tx.clone(); + let mut release_rx = release_rx.clone(); + assert!(spawn_background_publication(async move { + started_tx.send(std::thread::current().id()).unwrap(); + while !*release_rx.borrow() { + release_rx.changed().await.unwrap(); + } + })); + } + drop(started_tx); + let threads = (0..32) + .map(|_| { + started_rx + .recv_timeout(std::time::Duration::from_secs(2)) + .expect("background publication should start") + }) + .collect::>(); + assert_eq!( + threads.len(), + 1, + "detached publications must not allocate one OS thread per future" + ); + release_tx.send(true).unwrap(); + } } } @@ -805,6 +912,15 @@ pub(crate) async fn with_async_publication_context( native::with_async_publication_context(publication, future).await } +/// Schedule detached stream-finalization publication on the process-local +/// executor. The executor uses one shared OS thread and is reset after fork. +pub(crate) fn spawn_background_publication(future: F) -> bool +where + F: Future + Send + 'static, +{ + native::spawn_background_publication(future) +} + /// Wait for all queued subscriber callbacks submitted before this call. pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index e9b042f50..c83ad264e 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -333,33 +333,24 @@ impl LlmStreamWrapper { ); } }; - let finalize = - subscriber_dispatcher::with_async_publication_context(publication_barrier, finalize); + let publication_context = subscriber_dispatcher::capture_publication_context(); + let finalize = subscriber_dispatcher::with_task_publication_context( + publication_context, + subscriber_dispatcher::with_async_publication_context(publication_barrier, finalize), + ); if background_thread { - // `Drop` can run while the current-thread Tokio executor is - // synchronously flushing subscribers. Use a dedicated runtime so - // the FIFO publication barrier can still be released. - std::thread::spawn(move || { - if let Ok(runtime) = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - runtime.block_on(finalize); - } - }); + // `Drop` cannot await middleware and may run while the caller's + // executor is synchronously flushing subscribers. A process-local + // executor polls all detached finalizers on one shared OS thread. + // Pending middleware therefore does not create one thread per + // abandoned stream. + let _ = subscriber_dispatcher::spawn_background_publication(finalize); return None; } match tokio::runtime::Handle::try_current() { Ok(handle) => Some(handle.spawn(finalize)), Err(_) => { - std::thread::spawn(move || { - if let Ok(runtime) = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - runtime.block_on(finalize); - } - }); + let _ = subscriber_dispatcher::spawn_background_publication(finalize); None } } diff --git a/crates/core/tests/unit/llm_api_tests.rs b/crates/core/tests/unit/llm_api_tests.rs index 8e90747da..30a357465 100644 --- a/crates/core/tests/unit/llm_api_tests.rs +++ b/crates/core/tests/unit/llm_api_tests.rs @@ -14,8 +14,8 @@ use tokio_stream::StreamExt; use super::{ CreateLlmHandleParams, LlmCallEndParams, LlmCallExecuteParams, LlmCallParams, LlmHandle, LlmRequest, LlmStreamCallExecuteParams, create_llm_handle, emit_llm_start, - emit_optimization_marks_with, llm_call, llm_call_end, llm_call_execute, - llm_stream_call_execute, project_llm_request_to_current_user_turn, + emit_optimization_marks_with, enqueue_optimization_marks, llm_call, llm_call_end, + llm_call_execute, llm_stream_call_execute, project_llm_request_to_current_user_turn, sanitize_context_for_request_codec, sanitize_context_for_response_codec, }; use crate::api::event::{Event, ScopeCategory}; @@ -1476,6 +1476,41 @@ fn unavailable_mark_sanitizer_does_not_acknowledge_the_delivery_cursor() { assert!(handle.optimization_recorder.unemitted().is_empty()); } +#[test] +fn manual_optimization_mark_snapshot_failure_publishes_fail_open() { + let _guard = lock_global_runtime(); + reset_global(); + let scope_stack = create_scope_stack(); + set_thread_scope_stack(scope_stack.clone()); + let handle = LlmHandle::builder().name("poisoned-mark-snapshot").build(); + assert!( + handle + .optimization_recorder + .record(LlmOptimizationContribution::new( + "test", + "snapshot_fail_open" + )) + ); + std::thread::spawn(move || { + let _guard = scope_stack.write().unwrap(); + panic!("poison the captured scope stack"); + }) + .join() + .unwrap_err(); + + let events = Arc::new(Mutex::new(Vec::new())); + let captured = events.clone(); + let subscribers = vec![Arc::new(move |event: &Event| { + captured.lock().unwrap().push(event.clone()); + }) as crate::api::runtime::EventSubscriberFn]; + enqueue_optimization_marks(&handle, &subscribers); + flush_subscribers().unwrap(); + + assert_eq!(events.lock().unwrap().len(), 1); + assert!(handle.optimization_recorder.unemitted().is_empty()); + set_thread_scope_stack(create_scope_stack()); +} + #[test] fn close_boundary_freezes_identical_mark_and_summary_contributions() { let _guard = lock_global_runtime(); diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index fa9e28bd0..b61d82696 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -1647,7 +1647,14 @@ pub fn create_scope_stack_from_propagation( /// The stack is restored before this function returns. Asynchronous callbacks /// must not rely on this installation after their first `await`. #[napi] -pub fn with_scope_stack(stack: &ScopeStack, callback: JsFunction) -> napi::Result { +pub fn with_scope_stack( + env: Env, + stack: &ScopeStack, + callback: JsFunction, +) -> napi::Result { + if let Some(value) = callback_factory::with_callback_scope_stack(&env, stack, &callback)? { + return Ok(value); + } with_scope_stack_handle(stack.inner.clone(), || { callback.call::(None, &[]) }) @@ -1663,8 +1670,12 @@ pub fn current_scope_stack(env: Env) -> napi::Result { /// Binds a scope stack to the current thread. #[napi] -pub fn set_thread_scope_stack(stack: &ScopeStack) { +pub fn set_thread_scope_stack(env: Env, stack: &ScopeStack) -> napi::Result<()> { + if callback_factory::set_callback_scope_stack(&env, stack)? { + return Ok(()); + } bind_thread_scope_stack(stack.inner.clone()); + Ok(()) } /// Returns whether the current execution context has an explicitly-initialized diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index c3f35be6b..a9e673a37 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -479,9 +479,8 @@ pub fn wrap_js_llm_request_intercept_promise_fn( /// Wrap a Promise-aware JS event sanitizer. /// -/// Scope and mark publication invokes these callbacks from Relay's serial -/// dispatcher, while managed tool/LLM lifecycle paths can invoke them inline. -/// The invocation context decides whether `flushSubscribers()` is reentrant. +/// All lifecycle publication invokes these callbacks from Relay's serial +/// dispatcher. The invocation context marks `flushSubscribers()` as reentrant. pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSanitizeFn { Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index 92a84d894..093ff679f 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -123,6 +123,24 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { callbackScopeStack() { return eventSanitizerContext.getStore()?.scopeStack; }, + + withCallbackScopeStack(scopeStack, fn) { + const current = eventSanitizerContext.getStore(); + if (current === undefined) { + return { active: false }; + } + const token = { active: current.active, scopeStack }; + return { active: true, value: eventSanitizerContext.run(token, fn) }; + }, + + setCallbackScopeStack(scopeStack) { + const current = eventSanitizerContext.getStore(); + if (current === undefined) { + return false; + } + current.scopeStack = scopeStack; + return true; + }, }; })()"#; @@ -189,3 +207,29 @@ pub(crate) fn callback_scope_stack(env: &Env) -> napi::Result::from_napi_value(env.raw(), value.raw())? }; Ok(Some(stack.inner.clone())) } + +pub(crate) fn with_callback_scope_stack( + env: &Env, + stack: &ScopeStack, + callback: &JsFunction, +) -> napi::Result> { + let factories = callback_factories(env)?; + let with_stack: JsFunction = factories.get_named_property("withCallbackScopeStack")?; + let stack = ScopeStack::from(stack.inner.clone()).into_instance(*env)?; + let outcome = with_stack.call(None, &[as_unknown(env, &stack), as_unknown(env, callback)])?; + let outcome = unsafe { JsObject::from_raw_unchecked(env.raw(), outcome.raw()) }; + if !outcome.get_named_property::("active")? { + return Ok(None); + } + outcome.get_named_property("value").map(Some) +} + +pub(crate) fn set_callback_scope_stack(env: &Env, stack: &ScopeStack) -> napi::Result { + let factories = callback_factories(env)?; + let set_stack: JsFunction = factories.get_named_property("setCallbackScopeStack")?; + let stack = ScopeStack::from(stack.inner.clone()).into_instance(*env)?; + set_stack + .call::(None, &[as_unknown(env, &stack)])? + .coerce_to_bool()? + .get_value() +} diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index 5bab8776d..bf77cc447 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -374,7 +374,9 @@ impl PromiseAwareFn { spread: mode.spread, next, publication: mode.publication, - scope_stack: mode.publication.then(current_scope_stack), + // Scope identity applies to every middleware callback. The + // publication bit controls only re-entrant flush behavior. + scope_stack: Some(current_scope_stack()), completion: CallCompletion::new(sender), }), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 82c48847e..aaa85832e 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -134,8 +134,11 @@ describe('event sanitizer registries', () => { const originalStack = lib.currentScopeStack(); const emitterStack = lib.createScopeStack(); const unrelatedStack = lib.createScopeStack(); + const overrideStack = lib.createScopeStack(); + let overrideRootUuid; let emitterScopeUuid; const observedParents = []; + const observedOverrides = []; lib.registerMarkSanitizeGuardrail('node-event-scope-context', 0, async (event, fields) => { if (event.name !== 'scope-context-original') { @@ -145,10 +148,16 @@ describe('event sanitizer registries', () => { await new Promise((resolve) => setImmediate(resolve)); observedParents.push(lib.getHandle().uuid); lib.event('scope-context-nested', null, { originalParent: event.parent_uuid }); + observedOverrides.push( + lib.withScopeStack(overrideStack, () => lib.getHandle().uuid), + ); + lib.setThreadScopeStack(overrideStack); + observedOverrides.push(lib.getHandle().uuid); return fields; }); try { + overrideRootUuid = lib.withScopeStack(overrideStack, () => lib.getHandle().uuid); lib.withScopeStack(emitterStack, () => { emitterScopeUuid = lib.pushScope('scope-context-emitter', lib.ScopeType.Agent).uuid; lib.event('scope-context-original', null, {}); @@ -161,6 +170,7 @@ describe('event sanitizer registries', () => { await waitFor(events, 2); assert.deepEqual(observedParents, [emitterScopeUuid, emitterScopeUuid]); + assert.deepEqual(observedOverrides, [overrideRootUuid, overrideRootUuid]); const nested = events.find((event) => event.name === 'scope-context-nested'); assert.equal(nested.parent_uuid, emitterScopeUuid); assert.notEqual(nested.parent_uuid, unrelatedRootUuid); diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index 1fe913fd8..a83887d05 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -607,6 +607,33 @@ describe('Tool guardrails', () => { } }); + it('Promise middleware preserves its invocation scope before and after await', async () => { + const originalStack = lib.currentScopeStack(); + const invocationStack = lib.createScopeStack(); + const unrelatedStack = lib.createScopeStack(); + const observed = []; + let invocationScope; + lib.registerToolConditionalExecutionGuardrail('node_tool_cond_scope_context', 10, async () => { + observed.push(lib.getHandle().uuid); + await new Promise((resolve) => setImmediate(resolve)); + observed.push(lib.getHandle().uuid); + return null; + }); + try { + const execution = lib.withScopeStack(invocationStack, () => { + invocationScope = lib.pushScope('middleware-invocation', lib.ScopeType.Agent); + return lib.toolCallExecute('tool_cond_scope_context', {}, (args) => args); + }); + lib.setThreadScopeStack(unrelatedStack); + await execution; + assert.deepEqual(observed, [invocationScope.uuid, invocationScope.uuid]); + } finally { + lib.withScopeStack(invocationStack, () => lib.popScope(invocationScope)); + lib.setThreadScopeStack(originalStack); + lib.deregisterToolConditionalExecutionGuardrail('node_tool_cond_scope_context'); + } + }); + it('conditional guardrail propagates a rejected Promise', async () => { registerToolConditionalExecutionGuardrail('node_tool_cond_reject', 10, async () => { throw new Error('guardrail rejected promise'); diff --git a/python/nemo_relay/llm.py b/python/nemo_relay/llm.py index 4a9cfd269..96b2a8b55 100644 --- a/python/nemo_relay/llm.py +++ b/python/nemo_relay/llm.py @@ -160,14 +160,16 @@ def call_end( end event. When omitted, the runtime default end timestamp is used. Returns: - None: This function returns after the end event has been recorded. + None: This function returns after an immutable end-event snapshot and + its middleware/subscriber chains have been queued for publication. Notes: - ``call_end()`` applies sanitize-response guardrails to the emitted - end-event payload. ``response_codec`` and ``annotated_response`` enrich - observability output only and do not rewrite the recorded response. - Response codec failures are raised after the end event is emitted - without an annotation. + ``call_end()`` remains synchronous. Sanitize-response guardrails, + response-codec annotation, event sanitizers, and subscriber delivery + run later on Relay's serial publication path. Callback and codec + failures are logged and fail open; they cannot be raised by this call. + ``response_codec`` and ``annotated_response`` enrich observability + output only and do not rewrite the caller-owned response. ``timestamp`` must be a timezone-aware ``datetime``; strings and naive datetimes are rejected. """ diff --git a/python/nemo_relay/tools.py b/python/nemo_relay/tools.py index a3218136d..51995c059 100644 --- a/python/nemo_relay/tools.py +++ b/python/nemo_relay/tools.py @@ -121,11 +121,15 @@ def call_end(handle, result, *, data=None, metadata=None, timestamp: datetime | end event. When omitted, the runtime default end timestamp is used. Returns: - None: This function returns after the end event has been recorded. + None: This function returns after an immutable end-event snapshot and + its middleware/subscriber chains have been queued for publication. Notes: - ``call_end()`` applies sanitize-response guardrails to the emitted - end-event payload but does not alter the caller-owned ``result`` object. + ``call_end()`` remains synchronous. Sanitize-response guardrails, + event sanitizers, and subscriber delivery run later on Relay's serial + publication path. Callback failures are logged and fail open; they + cannot be raised by this call. The caller-owned ``result`` is not + altered. ``timestamp`` must be a timezone-aware ``datetime``; strings and naive datetimes are rejected. """ diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index 0ed04bde7..77ca9fbac 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -4,6 +4,7 @@ """Tests for NeMo Relay LLM lifecycle, guardrails, intercepts, and streaming.""" import asyncio +import contextvars from collections.abc import AsyncIterator from typing import NoReturn, cast @@ -755,6 +756,43 @@ async def stream_func(request) -> AsyncIterator[dict]: end = _llm_event(events, "stream_async_response_sanitizer", "end") assert end.data == {"sanitized": True} + async def test_stream_response_sanitizer_preserves_emitter_contextvars(self): + request_id = contextvars.ContextVar("stream_request_id", default="registration") + observed = [] + + async def sanitize_response(response, context): + del context + observed.append(request_id.get()) + await asyncio.sleep(0) + observed.append(request_id.get()) + return response + + async def stream_func(request): + del request + yield {"token": "hello"} + + guardrails.register_llm_sanitize_response( + "py_llm_stream_contextvars", + 1, + sanitize_response, + ) + token = request_id.set("caller") + try: + stream = await llm.stream_execute( + "stream_contextvars", + make_request(), + stream_func, + lambda _chunk: None, + lambda: {"done": True}, + ) + assert [chunk async for chunk in stream] == [{"token": "hello"}] + await subscribers.flush_async() + finally: + request_id.reset(token) + guardrails.deregister_llm_sanitize_response("py_llm_stream_contextvars") + + assert observed == ["caller", "caller"] + async def test_stream_execute_aclose_stops_partially_consumed_stream(self): producer_closed = asyncio.Event() wait_for_more_chunks = asyncio.Event() From bec88536732e93a5335891688015f13e50e8f615 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 03:03:49 -0400 Subject: [PATCH 45/83] fix(node): expire nested middleware context Signed-off-by: Will Killian --- crates/core/src/api/scope.rs | 3 ++- crates/node/src/callback_factory.rs | 18 +++++++++++------- crates/node/src/promise_call.rs | 2 +- crates/node/tests/event_sanitizers_tests.mjs | 13 ++++++++----- python/nemo_relay/scope.py | 5 +++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/crates/core/src/api/scope.rs b/crates/core/src/api/scope.rs index 3362c303a..dd5cf9154 100644 --- a/crates/core/src/api/scope.rs +++ b/crates/core/src/api/scope.rs @@ -357,7 +357,8 @@ pub fn pop_scope(params: PopScopeParams<'_>) -> Result<()> { /// `None`, the current UTC time is used. /// /// # Returns -/// A [`Result`] that is `Ok(())` after the event has been emitted. +/// A [`Result`] that is `Ok(())` after the event has been queued for +/// sanitization and publication. /// /// # Errors /// Returns an error when the runtime owner check fails or when internal state diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index 093ff679f..c4e2c5714 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -9,7 +9,7 @@ use nemo_relay::api::runtime::ScopeStackHandle; use crate::types::ScopeStack; -const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v2"; +const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v3"; const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { const { AsyncLocalStorage } = process.getBuiltinModule('node:async_hooks'); @@ -57,18 +57,18 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { } function callPromise(fn, arg0, spread, next, resolve, reject, publication, scopeStack) { - const token = { active: publication, scopeStack }; + const token = { publicationState: { active: publication }, scopeStack }; const invoke = () => { Promise.resolve().then(() => ( next === undefined ? (spread ? fn(...arg0) : fn(arg0)) : (spread ? fn(...arg0, next) : fn(arg0, next)) )).then((value) => jsonValue(value === undefined ? null : value)).then((value) => { - token.active = false; + token.publicationState.active = false; token.scopeStack = null; resolve(value); }, (error) => { - token.active = false; + token.publicationState.active = false; token.scopeStack = null; let message = 'unknown error'; try { @@ -117,7 +117,7 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { }, eventSanitizerCallbackActive() { - return eventSanitizerContext.getStore()?.active === true; + return eventSanitizerContext.getStore()?.publicationState.active === true; }, callbackScopeStack() { @@ -129,8 +129,12 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { if (current === undefined) { return { active: false }; } - const token = { active: current.active, scopeStack }; - return { active: true, value: eventSanitizerContext.run(token, fn) }; + const token = { publicationState: current.publicationState, scopeStack }; + try { + return { active: true, value: eventSanitizerContext.run(token, fn) }; + } finally { + token.scopeStack = null; + } }, setCallbackScopeStack(scopeStack) { diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index bf77cc447..95b377c7e 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -58,7 +58,7 @@ struct CallArgs { spread: bool, next: Option, publication: bool, - /// Scope stack installed by Relay while queued publication middleware runs. + /// Scope stack captured when Relay registers or invokes the middleware. scope_stack: Option, completion: CallCompletion, } diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index aaa85832e..3efd8dd5d 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -314,6 +314,7 @@ describe('event sanitizer registries', () => { it('clears sanitizer re-entrancy in async descendants after settlement', async () => { const events = capture('node-event-sanitize-descendant-flush-sub'); + const nestedStack = lib.createScopeStack(); let secondSanitizerEntered; const secondEntered = new Promise((resolve) => { secondSanitizerEntered = resolve; @@ -332,11 +333,13 @@ describe('event sanitizer registries', () => { }); lib.registerMarkSanitizeGuardrail('node-event-descendant-flush', 0, async (event, fields) => { if (event.name === 'descendant-flush-origin') { - setTimeout(async () => { - await secondEntered; - lib.flushSubscribers().then(descendantFlush.resolve, descendantFlush.reject); - descendantFlushStarted(); - }, 0); + lib.withScopeStack(nestedStack, () => { + setTimeout(async () => { + await secondEntered; + lib.flushSubscribers().then(descendantFlush.resolve, descendantFlush.reject); + descendantFlushStarted(); + }, 0); + }); } else if (event.name === 'descendant-flush-blocked') { secondSanitizerEntered(); await releaseSecond; diff --git a/python/nemo_relay/scope.py b/python/nemo_relay/scope.py index 644bf6a57..da44633ea 100644 --- a/python/nemo_relay/scope.py +++ b/python/nemo_relay/scope.py @@ -164,11 +164,12 @@ def event( event. When omitted, the current runtime time is used. Returns: - None: This function returns after the event has been emitted. + None: This function returns after the event has been queued for + sanitization and publication. Notes: A scope stack is created automatically when needed before the event is - emitted through the native runtime. ``timestamp`` must be a + queued through the native runtime. ``timestamp`` must be a timezone-aware ``datetime``; strings and naive datetimes are rejected. """ _ensure_scope_stack() From 70d863804982933b766442a8c0768c27fb274537 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 03:25:53 -0400 Subject: [PATCH 46/83] fix: isolate async middleware contexts Signed-off-by: Will Killian --- crates/node/src/api/mod.rs | 383 ++++++++++--------- crates/node/src/promise_call.rs | 67 +++- crates/node/tests/event_sanitizers_tests.mjs | 56 +++ crates/node/tests/tools_tests.mjs | 38 ++ crates/python/src/py_callable.rs | 103 +++-- python/tests/test_llm.py | 45 +++ 6 files changed, 482 insertions(+), 210 deletions(-) diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index b61d82696..7e670369a 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -81,6 +81,7 @@ use crate::convert::{ record_callback_error, to_napi_err, }; use crate::promise_call::PromiseAwareFn; +use crate::promise_call::with_publication_callback_context; use crate::stream::LlmStream; use crate::types::{LlmHandle, ScopeHandle, ScopeStack, ScopeType, ToolHandle}; @@ -1915,6 +1916,7 @@ pub fn with_scope( input: Option, ) -> Result { let attrs = ScopeAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; let scope_handle = with_scope_stack_handle(scope_stack.clone(), || { core_scope_api::push_scope( @@ -1961,34 +1963,37 @@ pub fn with_scope( env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let build_handle: crate::promise_call::Arg0Builder = - Box::new(move |env: &Env| { - let raw = unsafe { - ::to_napi_value( - env.raw(), - ScopeHandle::from(callback_handle), - )? - }; - Ok(unsafe { JsUnknown::from_raw_unchecked(env.raw(), raw) }) - }); - - let result = pa_fn.call_with_arg0(build_handle).await; - let metadata = match &result { - Ok(_) => otel_status_metadata("OK", None), - Err(error) => otel_status_metadata("ERROR", Some(error.to_string())), - }; - // Always pop the scope, even on error. - let _ = core_scope_api::pop_scope( - core_scope_api::PopScopeParams::builder() - .handle_uuid(&scope_uuid) - .metadata_opt(Some(metadata)) - .build(), - ); - result.map_err(to_napi_err) - }) - .await + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let build_handle: crate::promise_call::Arg0Builder = + Box::new(move |env: &Env| { + let raw = unsafe { + ::to_napi_value( + env.raw(), + ScopeHandle::from(callback_handle), + )? + }; + Ok(unsafe { JsUnknown::from_raw_unchecked(env.raw(), raw) }) + }); + + let result = pa_fn.call_with_arg0(build_handle).await; + let metadata = match &result { + Ok(_) => otel_status_metadata("OK", None), + Err(error) => otel_status_metadata("ERROR", Some(error.to_string())), + }; + // Always pop the scope, even on error. + let _ = core_scope_api::pop_scope( + core_scope_api::PopScopeParams::builder() + .handle_uuid(&scope_uuid) + .metadata_opt(Some(metadata)) + .build(), + ); + result.map_err(to_napi_err) + }) + .await + }) + .await }, |_env, result| Ok(result), ) @@ -2125,6 +2130,7 @@ pub fn tool_call_execute( metadata: Option, ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2135,23 +2141,26 @@ pub fn tool_call_execute( env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_call_execute( - core_tool_api::ToolCallExecuteParams::builder() - .name(name) - .args(args) - .func(default_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .build(), - ) + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_call_execute( + core_tool_api::ToolCallExecuteParams::builder() + .name(name) + .args(args) + .func(default_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .build(), + ) + .await + .map_err(to_napi_err) + }) .await - .map_err(to_napi_err) - }) - .await + }) + .await }, |_env, result| Ok(result), ) @@ -2178,6 +2187,7 @@ pub fn tool_call_execute_async( metadata: Option, ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2197,23 +2207,26 @@ pub fn tool_call_execute_async( env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_call_execute( - core_tool_api::ToolCallExecuteParams::builder() - .name(name) - .args(args) - .func(exec_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .build(), - ) + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_call_execute( + core_tool_api::ToolCallExecuteParams::builder() + .name(name) + .args(args) + .func(exec_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .build(), + ) + .await + .map_err(to_napi_err) + }) .await - .map_err(to_napi_err) - }) - .await + }) + .await }, |_env, result| Ok(result), ) @@ -2326,6 +2339,7 @@ pub fn llm_call_execute( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2359,25 +2373,28 @@ pub fn llm_call_execute( }); env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let params = core_llm_api::LlmCallExecuteParams::builder() - .name(name) - .request(llm_request) - .func(default_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .model_name_opt(model_name) - .codec_opt(codec) - .response_codec_opt(response_codec) - .build(); - core_llm_api::llm_call_execute(params) - .await - .map_err(to_napi_err) - }) - .await + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let params = core_llm_api::LlmCallExecuteParams::builder() + .name(name) + .request(llm_request) + .func(default_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .model_name_opt(model_name) + .codec_opt(codec) + .response_codec_opt(response_codec) + .build(); + core_llm_api::llm_call_execute(params) + .await + .map_err(to_napi_err) + }) + .await + }) + .await }, move |_env, result| { drop(codec_references); @@ -2407,6 +2424,7 @@ pub fn llm_call_execute_async( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2450,25 +2468,28 @@ pub fn llm_call_execute_async( env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let params = core_llm_api::LlmCallExecuteParams::builder() - .name(name) - .request(llm_request) - .func(exec_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .model_name_opt(model_name) - .codec_opt(codec) - .response_codec_opt(response_codec) - .build(); - core_llm_api::llm_call_execute(params) - .await - .map_err(to_napi_err) - }) - .await + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let params = core_llm_api::LlmCallExecuteParams::builder() + .name(name) + .request(llm_request) + .func(exec_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .model_name_opt(model_name) + .codec_opt(codec) + .response_codec_opt(response_codec) + .build(); + core_llm_api::llm_call_execute(params) + .await + .map_err(to_napi_err) + }) + .await + }) + .await }, move |_env, result| { drop(codec_references); @@ -2513,6 +2534,7 @@ pub fn llm_stream_call_execute( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2589,44 +2611,45 @@ pub fn llm_stream_call_execute( let completion_codec_references = codec_references.clone(); env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let params = core_llm_api::LlmStreamCallExecuteParams::builder() - .name(name) - .request(llm_request) - .func(default_fn) - .collector(wrapped_collector) - .finalizer(wrapped_finalizer) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .model_name_opt(model_name) - .codec_opt(codec) - .response_codec_opt(response_codec) - .build(); - let rust_stream = core_llm_api::llm_stream_call_execute(params) - .await - .map_err(to_napi_err)?; - - let (tx, rx) = tokio::sync::mpsc::channel(32); - let (cancel, cancel_rx) = tokio::sync::watch::channel(false); - let (closed, closed_rx) = tokio::sync::watch::channel(None); - tokio::spawn(forward_stream_to_channel( - rust_stream, - tx, - cancel_rx, - closed, - )); - - Ok(LlmStream { - receiver: tokio::sync::Mutex::new(rx), - cancel, - closed: closed_rx, - codec_references, + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let params = core_llm_api::LlmStreamCallExecuteParams::builder() + .name(name) + .request(llm_request) + .func(default_fn) + .collector(wrapped_collector) + .finalizer(wrapped_finalizer) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .model_name_opt(model_name) + .codec_opt(codec) + .response_codec_opt(response_codec) + .build(); + let rust_stream = core_llm_api::llm_stream_call_execute(params) + .await + .map_err(to_napi_err)?; + + let (tx, rx) = tokio::sync::mpsc::channel(32); + let (cancel, cancel_rx) = tokio::sync::watch::channel(false); + let (closed, closed_rx) = tokio::sync::watch::channel(None); + tokio::spawn(with_publication_callback_context( + publication_callback_active, + forward_stream_to_channel(rust_stream, tx, cancel_rx, closed), + )); + + Ok(LlmStream { + receiver: tokio::sync::Mutex::new(rx), + cancel, + closed: closed_rx, + codec_references, + }) }) - }) - .await + .await + }) + .await }, move |_env, result| { drop(completion_codec_references); @@ -2646,7 +2669,7 @@ macro_rules! napi_event_guardrail_api { /// The callback may return fields directly or in a Promise. Scope and mark /// calls queue the event and return synchronously; publication resumes after /// the Promise settles. Callback, serialization, conversion, or invalid-result - /// failures preserve the original event fields and record the error for + /// failures preserve the last valid event fields and record the error for /// `getLastCallbackError()`. #[napi] pub fn $register_name( @@ -3177,7 +3200,7 @@ macro_rules! napi_scope_event_guardrail_api { /// The callback may return fields directly or in a Promise. Scope and mark /// calls queue the event and return synchronously; publication resumes after /// the Promise settles. Callback, serialization, conversion, or invalid-result - /// failures preserve the original event fields and record the error for + /// failures preserve the last valid event fields and record the error for /// `getLastCallbackError()`. #[napi] pub fn $register_name( @@ -3776,16 +3799,20 @@ pub fn scope_deregister_subscriber(scope_uuid: String, name: String) -> Result Result { + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_request_intercepts(&name, args) - .await - .map_err(to_napi_err) - }) - .await + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_request_intercepts(&name, args) + .await + .map_err(to_napi_err) + }) + .await + }) + .await }, |_env, result| Ok(result), ) @@ -3795,16 +3822,20 @@ pub fn tool_request_intercepts(env: Env, name: String, args: Json) -> Result Result { + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_conditional_execution(&name, &args) - .await - .map_err(to_napi_err) - }) - .await + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_conditional_execution(&name, &args) + .await + .map_err(to_napi_err) + }) + .await + }) + .await }, |env, _| env.get_undefined(), ) @@ -3819,24 +3850,28 @@ pub fn tool_conditional_execution(env: Env, name: String, args: Json) -> Result< pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result { let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_llm_api::llm_request_intercepts(&name, llm_request) - .await - .map(|r| { - serde_json::json!({ - "request": r.request, - "annotated": r.annotated_request, - "pendingMarks": callable::js_pending_marks(r.pending_marks), - "optimizationContributions": r.optimization_contributions, + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_llm_api::llm_request_intercepts(&name, llm_request) + .await + .map(|r| { + serde_json::json!({ + "request": r.request, + "annotated": r.annotated_request, + "pendingMarks": callable::js_pending_marks(r.pending_marks), + "optimizationContributions": r.optimization_contributions, + }) }) - }) - .map_err(to_napi_err) - }) - .await + .map_err(to_napi_err) + }) + .await + }) + .await }, |_env, result| Ok(result), ) @@ -3849,16 +3884,20 @@ pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result Result { let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; + let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_llm_api::llm_conditional_execution(&llm_request) - .await - .map_err(to_napi_err) - }) - .await + with_publication_callback_context(publication_callback_active, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_llm_api::llm_conditional_execution(&llm_request) + .await + .map_err(to_napi_err) + }) + .await + }) + .await }, |env, _| env.get_undefined(), ) diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index 95b377c7e..b222edd53 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -20,12 +20,29 @@ use napi::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}; use napi::{Env, JsFunction, JsUnknown, NapiRaw, NapiValue}; use serde_json::Value as Json; -use nemo_relay::api::runtime::{ScopeStackHandle, current_scope_stack}; +use nemo_relay::api::runtime::{ScopeStackHandle, TASK_SCOPE_STACK, current_scope_stack}; use nemo_relay::error::{FlowError, Result as FlowResult}; use crate::callback_factory; use crate::types::ScopeStack; +tokio::task_local! { + static PUBLICATION_CALLBACK_ACTIVE: bool; +} + +pub(crate) async fn with_publication_callback_context( + active: bool, + future: F, +) -> F::Output { + PUBLICATION_CALLBACK_ACTIVE.scope(active, future).await +} + +fn publication_callback_active() -> bool { + PUBLICATION_CALLBACK_ACTIVE + .try_with(|active| *active) + .unwrap_or(false) +} + pub type JsonNextFn = Arc Pin> + Send>> + Send + Sync>; pub type JsonStreamNextFn = @@ -58,7 +75,7 @@ struct CallArgs { spread: bool, next: Option, publication: bool, - /// Scope stack captured when Relay registers or invokes the middleware. + /// Scope stack captured when Relay invokes the middleware. scope_stack: Option, completion: CallCompletion, } @@ -131,17 +148,30 @@ fn undefined_to_unknown(env: &Env) -> napi::Result { Ok(unsafe { JsUnknown::from_raw_unchecked(env.raw(), value.raw()) }) } -fn build_next_unknown(env: &Env, next: NextFn) -> napi::Result { +fn build_next_unknown( + env: &Env, + next: NextFn, + scope_stack: ScopeStackHandle, + publication: bool, +) -> napi::Result { let next_fn = match next { NextFn::Json(next) => { env.create_function_from_closure("__nemo_relay_next", move |ctx| { let arg = ctx.get::(0).unwrap_or(Json::Null); let next = next.clone(); + let scope_stack = scope_stack.clone(); ctx.env.execute_tokio_future( async move { - next(arg) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) + with_publication_callback_context(publication, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + next(arg) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + }) + .await + }) + .await }, |_env, value| Ok(value), ) @@ -151,11 +181,19 @@ fn build_next_unknown(env: &Env, next: NextFn) -> napi::Result { env.create_function_from_closure("__nemo_relay_next", move |ctx| { let arg = ctx.get::(0).unwrap_or(Json::Null); let next = next.clone(); + let scope_stack = scope_stack.clone(); ctx.env.execute_tokio_future( async move { - next(arg) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) + with_publication_callback_context(publication, async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + next(arg) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + }) + .await + }) + .await }, |_env, value| Ok(value), ) @@ -217,7 +255,14 @@ impl PromiseAwareFn { let mut tsfn = env.create_threadsafe_function(wrapper, 0, |ctx: ThreadSafeCallContext| { let next = match ctx.value.next { - Some(next) => build_next_unknown(&ctx.env, next)?, + Some(next) => { + let scope_stack = ctx.value.scope_stack.clone().ok_or_else(|| { + napi::Error::from_reason( + "middleware next callback is missing its captured scope stack", + ) + })?; + build_next_unknown(&ctx.env, next, scope_stack, ctx.value.publication)? + } None => undefined_to_unknown(&ctx.env)?, }; let (resolve, reject) = build_completion_unknowns(&ctx.env, ctx.value.completion)?; @@ -373,7 +418,7 @@ impl PromiseAwareFn { arg0, spread: mode.spread, next, - publication: mode.publication, + publication: mode.publication || publication_callback_active(), // Scope identity applies to every middleware callback. The // publication bit controls only re-entrant flush behavior. scope_stack: Some(current_scope_stack()), diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 3efd8dd5d..6a104bc4e 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -234,6 +234,62 @@ describe('event sanitizer registries', () => { assert.equal(flushReturned, true); }); + it('preserves sanitizer re-entrancy through nested managed middleware', async () => { + const events = capture('node-event-sanitize-nested-middleware-sub'); + const middlewareFlushes = []; + lib.registerToolConditionalExecutionGuardrail( + 'node-event-nested-middleware-conditional', + 0, + async (name) => { + if (name === 'node-event-nested-middleware-tool') { + await lib.flushSubscribers(); + middlewareFlushes.push('conditional'); + } + return null; + }, + ); + lib.registerToolExecutionIntercept( + 'node-event-nested-middleware-outer', + 0, + async (args, next) => ({ result: await next(args) }), + ); + lib.registerToolExecutionIntercept( + 'node-event-nested-middleware-inner', + 10, + async (args, next) => { + await lib.flushSubscribers(); + middlewareFlushes.push('execution'); + return { result: await next(args) }; + }, + ); + lib.registerMarkSanitizeGuardrail( + 'node-event-nested-middleware-sanitizer', + 0, + async (event, fields) => { + if (event.name === 'nested-middleware-checkpoint') { + await lib.toolCallExecute('node-event-nested-middleware-tool', {}, (args) => args); + } + return fields; + }, + ); + try { + lib.event('nested-middleware-checkpoint', null, { raw: true }); + const state = await Promise.race([ + lib.flushSubscribers().then(() => 'flushed'), + new Promise((resolve) => setTimeout(() => resolve('blocked'), 500)), + ]); + assert.equal(state, 'flushed'); + await waitFor(events, 3); + } finally { + lib.deregisterMarkSanitizeGuardrail('node-event-nested-middleware-sanitizer'); + lib.deregisterToolConditionalExecutionGuardrail('node-event-nested-middleware-conditional'); + lib.deregisterToolExecutionIntercept('node-event-nested-middleware-outer'); + lib.deregisterToolExecutionIntercept('node-event-nested-middleware-inner'); + lib.deregisterSubscriber('node-event-sanitize-nested-middleware-sub'); + } + assert.deepEqual(middlewareFlushes, ['conditional', 'execution']); + }); + it('does not treat an unrelated flush as sanitizer re-entrancy', async () => { const events = capture('node-event-sanitize-independent-flush-sub'); let releaseSanitizer; diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index a83887d05..208f5e007 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -989,6 +989,44 @@ describe('Tool intercepts', () => { } }); + it('execution intercept next preserves the invocation scope across the chain', async () => { + const originalStack = lib.currentScopeStack(); + const invocationStack = lib.createScopeStack(); + const unrelatedStack = lib.createScopeStack(); + const observed = []; + let invocationScope; + + const intercept = (label) => async (args, next) => { + observed.push([label, 'before', lib.getHandle().uuid]); + await new Promise((resolve) => setImmediate(resolve)); + const result = await next(args); + observed.push([label, 'after', lib.getHandle().uuid]); + return { result }; + }; + + registerToolExecutionIntercept('node_tool_exec_scope_outer', 10, intercept('outer')); + registerToolExecutionIntercept('node_tool_exec_scope_inner', 20, intercept('inner')); + try { + const execution = lib.withScopeStack(invocationStack, () => { + invocationScope = lib.pushScope('execution-intercept-invocation', lib.ScopeType.Agent); + return toolCallExecute('execution_intercept_scope', {}, (args) => args); + }); + lib.setThreadScopeStack(unrelatedStack); + await execution; + assert.deepEqual(observed, [ + ['outer', 'before', invocationScope.uuid], + ['inner', 'before', invocationScope.uuid], + ['inner', 'after', invocationScope.uuid], + ['outer', 'after', invocationScope.uuid], + ]); + } finally { + lib.withScopeStack(invocationStack, () => lib.popScope(invocationScope)); + lib.setThreadScopeStack(originalStack); + deregisterToolExecutionIntercept('node_tool_exec_scope_outer'); + deregisterToolExecutionIntercept('node_tool_exec_scope_inner'); + } + }); + it('snapshotted execution intercept survives deregistration', async () => { let blockerEntered; const entered = new Promise((resolve) => { diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 1f851f4bf..cbd6e3412 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -240,12 +240,35 @@ fn running_task_locals(locals: TaskLocals) -> Option { live.then_some(locals) } +fn fresh_running_task_locals(locals: TaskLocals) -> Option { + let locals = running_task_locals(locals)?; + Python::attach(|py| { + let context = locals.context(py).call_method0("copy").ok()?; + Some(TaskLocals::new(locals.event_loop(py)).with_context(context)) + }) +} + fn task_locals_with_running_loop(registered: Option<&TaskLocals>) -> Option { publication_context::() .and_then(|context| context.task_locals.clone()) + .and_then(fresh_running_task_locals) + .or_else(|| capture_python_task_locals().and_then(fresh_running_task_locals)) + .or_else(|| registered.cloned().and_then(fresh_running_task_locals)) +} + +fn copy_publication_invocation<'py>( + py: Python<'py>, + context: &PythonPublicationContext, +) -> PyResult<(Bound<'py, PyAny>, Option)> { + let invocation_context = context.context.bind(py).call_method0("copy")?; + let task_locals = context + .task_locals + .clone() .and_then(running_task_locals) - .or_else(|| capture_python_task_locals().and_then(running_task_locals)) - .or_else(|| registered.cloned().and_then(running_task_locals)) + .map(|locals| { + TaskLocals::new(locals.event_loop(py)).with_context(invocation_context.clone()) + }); + Ok((invocation_context, task_locals)) } async fn resolve_py_object_or_future( @@ -546,22 +569,27 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = match publication_context.as_ref() { + Some(context) => { + let (context, publication_task_locals) = + copy_publication_invocation(py, context) + .map_err(|error| FlowError::Internal(error.to_string()))?; + (Some(context), publication_task_locals.or(task_locals)) + } + None => (None, task_locals), + }; let py_args = json_to_py(py, &args) .map_err(|e| FlowError::Internal(format!("tool json_to_py failed: {e}")))?; - let result = match (publication_context.as_ref(), publication) { + let result = match (invocation_context.as_ref(), publication) { (Some(context), true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .and_then(|invoke| { - context - .context - .bind(py) - .call_method1("run", (invoke, py_fn.bind(py), name, py_args)) + context.call_method1("run", (invoke, py_fn.bind(py), name, py_args)) }), - (Some(context), false) => context - .context - .bind(py) - .call_method1("run", (py_fn.bind(py), name, py_args)), + (Some(context), false) => { + context.call_method1("run", (py_fn.bind(py), name, py_args)) + } (None, true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) @@ -983,24 +1011,30 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = match publication_context.as_ref() { + Some(context) => { + let (context, publication_task_locals) = + copy_publication_invocation(py, context) + .map_err(|error| FlowError::Internal(error.to_string()))?; + (Some(context), publication_task_locals.or(task_locals)) + } + None => (None, task_locals), + }; let args = ( PyLLMRequest { inner: request }, PyLlmSanitizeRequestContext { inner: context }, ); - let result = match (publication_context.as_ref(), publication) { + let result = match (invocation_context.as_ref(), publication) { (Some(context), true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .and_then(|invoke| { context - .context - .bind(py) .call_method1("run", (invoke, py_fn.bind(py), args.0, args.1)) }), - (Some(context), false) => context - .context - .bind(py) - .call_method1("run", (py_fn.bind(py), args.0, args.1)), + (Some(context), false) => { + context.call_method1("run", (py_fn.bind(py), args.0, args.1)) + } (None, true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) @@ -1221,23 +1255,31 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = match publication_context.as_ref() { + Some(context) => { + let (context, publication_task_locals) = + copy_publication_invocation(py, context) + .map_err(|error| FlowError::Internal(error.to_string()))?; + (Some(context), publication_task_locals.or(task_locals)) + } + None => (None, task_locals), + }; let py_context = PyLlmSanitizeResponseContext { inner: context }; let py_response = json_to_py(py, &response) .map_err(|error| FlowError::Internal(error.to_string()))?; - let result = match (publication_context.as_ref(), publication) { + let result = match (invocation_context.as_ref(), publication) { (Some(context), true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .and_then(|invoke| { - context.context.bind(py).call_method1( + context.call_method1( "run", (invoke, py_fn.bind(py), py_response, py_context), ) }), - (Some(context), false) => context - .context - .bind(py) - .call_method1("run", (py_fn.bind(py), py_response, py_context)), + (Some(context), false) => { + context.call_method1("run", (py_fn.bind(py), py_response, py_context)) + } (None, true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) @@ -1309,6 +1351,15 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { Box::pin(async move { let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { + let (invocation_context, task_locals) = match publication_context.as_ref() { + Some(context) => { + let (context, publication_task_locals) = + copy_publication_invocation(py, context) + .map_err(|error| FlowError::Internal(error.to_string()))?; + (Some(context), publication_task_locals.or(task_locals)) + } + None => (None, task_locals), + }; let py_event = match event.as_ref() { Event::Scope(inner) => Py::new( py, @@ -1347,10 +1398,8 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .map_err(|error| FlowError::Internal(error.to_string()))?; - let result = match publication_context.as_ref() { + let result = match invocation_context.as_ref() { Some(context) => context - .context - .bind(py) .call_method1("run", (invoke, py_fn.bind(py), py_event, py_fields)), None => invoke.call1((py_fn.bind(py), py_event, py_fields)), } diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index 77ca9fbac..f3eb204b6 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -5,6 +5,7 @@ import asyncio import contextvars +import threading from collections.abc import AsyncIterator from typing import NoReturn, cast @@ -116,6 +117,50 @@ async def func(request): result = await llm.execute("async_method_llm", request, func) assert result["messages"] == [] + async def test_event_and_response_sanitizers_use_independent_context_snapshots(self): + events = [] + start_entered = threading.Event() + release_start = threading.Event() + response_called = False + + def sanitize_start(event, fields): + if event.name == "context_snapshot_llm": + start_entered.set() + assert release_start.wait(timeout=2) + return fields + + def sanitize_response(response, context): + nonlocal response_called + del response, context + response_called = True + return {"sanitized": True} + + async def provider(request): + del request + assert await asyncio.to_thread(start_entered.wait, 2) + return {"raw": True} + + subscribers.register("py_llm_context_snapshot_sub", events.append) + guardrails.register_scope_sanitize_start("py_llm_context_snapshot_start", 0, sanitize_start) + guardrails.register_llm_sanitize_response( + "py_llm_context_snapshot_response", + 0, + sanitize_response, + ) + try: + assert await llm.execute("context_snapshot_llm", make_request(), provider) == {"raw": True} + finally: + release_start.set() + guardrails.deregister_scope_sanitize_start("py_llm_context_snapshot_start") + guardrails.deregister_llm_sanitize_response("py_llm_context_snapshot_response") + try: + await subscribers.flush_async() + finally: + subscribers.deregister("py_llm_context_snapshot_sub") + + assert response_called + assert _llm_event(events, "context_snapshot_llm", "end").data == {"sanitized": True} + class TestLLMGuardrails: @pytest.mark.parametrize( From 6ac0f7c26db554eb7e935185d6bc416d5d3137db Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 03:41:57 -0400 Subject: [PATCH 47/83] fix: preserve queued middleware context Signed-off-by: Will Killian --- crates/core/src/stream.rs | 5 +- crates/node/src/api/mod.rs | 42 ++++---- crates/node/src/callback_factory.rs | 106 +++++++++++++++++-- crates/node/src/promise_call.rs | 40 ++++--- crates/node/tests/event_sanitizers_tests.mjs | 65 ++++++++++++ crates/node/tests/llm_tests.mjs | 47 ++++++++ crates/python/src/py_callable.rs | 89 +++++++++++----- python/tests/test_event_sanitizers.py | 29 +++++ 8 files changed, 351 insertions(+), 72 deletions(-) diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index c83ad264e..aaa5b3376 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -41,7 +41,8 @@ use crate::api::runtime::NemoRelayContextState; use crate::api::runtime::global_context; use crate::api::runtime::subscriber_dispatcher; use crate::api::runtime::{ - EventSubscriberFn, LlmJsonStream, LlmStreamInner, ScopeStackHandle, current_scope_stack, + EventSubscriberFn, LlmJsonStream, LlmStreamInner, ScopeStackHandle, TASK_SCOPE_STACK, + current_scope_stack, }; use crate::api::shared::{metadata_with_otel_status, snapshot_event_sanitizers}; use crate::codec::response::{AnnotatedLlmResponse, attach_estimated_cost_for_provider}; @@ -262,6 +263,7 @@ impl LlmStreamWrapper { }; let handle = self.handle.clone(); let scope_stack = self.scope_stack.clone(); + let finalization_scope_stack = scope_stack.clone(); let subscribers = self.subscribers.clone(); let response_codec = self.response_codec.clone(); let sanitize_context = self.sanitize_context.clone(); @@ -333,6 +335,7 @@ impl LlmStreamWrapper { ); } }; + let finalize = TASK_SCOPE_STACK.scope(finalization_scope_stack, finalize); let publication_context = subscriber_dispatcher::capture_publication_context(); let finalize = subscriber_dispatcher::with_task_publication_context( publication_context, diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 7e670369a..7d25e7170 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -1916,7 +1916,7 @@ pub fn with_scope( input: Option, ) -> Result { let attrs = ScopeAttributes::from_bits_truncate(attributes.unwrap_or(0)); - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; let scope_handle = with_scope_stack_handle(scope_stack.clone(), || { core_scope_api::push_scope( @@ -1963,7 +1963,7 @@ pub fn with_scope( env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { let build_handle: crate::promise_call::Arg0Builder = @@ -2130,7 +2130,7 @@ pub fn tool_call_execute( metadata: Option, ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2141,7 +2141,7 @@ pub fn tool_call_execute( env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { core_tool_api::tool_call_execute( @@ -2187,7 +2187,7 @@ pub fn tool_call_execute_async( metadata: Option, ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2207,7 +2207,7 @@ pub fn tool_call_execute_async( env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { core_tool_api::tool_call_execute( @@ -2339,7 +2339,7 @@ pub fn llm_call_execute( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2373,7 +2373,7 @@ pub fn llm_call_execute( }); env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { let params = core_llm_api::LlmCallExecuteParams::builder() @@ -2424,7 +2424,7 @@ pub fn llm_call_execute_async( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2468,7 +2468,7 @@ pub fn llm_call_execute_async( env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { let params = core_llm_api::LlmCallExecuteParams::builder() @@ -2534,7 +2534,7 @@ pub fn llm_stream_call_execute( #[napi(ts_arg_type = "(arg: Json) => any")] response_codec_decode: Option, ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; let parent = handle .map(|h| h.inner.clone()) @@ -2611,7 +2611,7 @@ pub fn llm_stream_call_execute( let completion_codec_references = codec_references.clone(); env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id.clone(), async move { TASK_SCOPE_STACK .scope(scope_stack, async move { let params = core_llm_api::LlmStreamCallExecuteParams::builder() @@ -2636,7 +2636,7 @@ pub fn llm_stream_call_execute( let (cancel, cancel_rx) = tokio::sync::watch::channel(false); let (closed, closed_rx) = tokio::sync::watch::channel(None); tokio::spawn(with_publication_callback_context( - publication_callback_active, + publication_context_id, forward_stream_to_channel(rust_stream, tx, cancel_rx, closed), )); @@ -3799,11 +3799,11 @@ pub fn scope_deregister_subscriber(scope_uuid: String, name: String) -> Result Result { - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { core_tool_api::tool_request_intercepts(&name, args) @@ -3822,11 +3822,11 @@ pub fn tool_request_intercepts(env: Env, name: String, args: Json) -> Result Result { - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { core_tool_api::tool_conditional_execution(&name, &args) @@ -3850,11 +3850,11 @@ pub fn tool_conditional_execution(env: Env, name: String, args: Json) -> Result< pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result { let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { core_llm_api::llm_request_intercepts(&name, llm_request) @@ -3884,11 +3884,11 @@ pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result Result { let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; - let publication_callback_active = callback_factory::event_sanitizer_callback_active(&env)?; + let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; let scope_stack = effective_scope_stack(&env)?; env.execute_tokio_future( async move { - with_publication_callback_context(publication_callback_active, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { core_llm_api::llm_conditional_execution(&llm_request) diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index c4e2c5714..a7c81e423 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -9,11 +9,13 @@ use nemo_relay::api::runtime::ScopeStackHandle; use crate::types::ScopeStack; -const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v3"; +const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v4"; const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { const { AsyncLocalStorage } = process.getBuiltinModule('node:async_hooks'); const eventSanitizerContext = new AsyncLocalStorage(); + const publicationStates = new Map(); + let nextPublicationContextId = 0; function jsonValue(value, seen = new Set()) { if (value === null || typeof value === 'string' || typeof value === 'boolean') { @@ -56,20 +58,59 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { return result; } - function callPromise(fn, arg0, spread, next, resolve, reject, publication, scopeStack) { - const token = { publicationState: { active: publication }, scopeStack }; + function callPromise( + fn, + arg0, + spread, + next, + resolve, + reject, + publication, + publicationContextId, + scopeStack, + ) { + let ownsPublicationState = false; + let publicationState; + if (publicationContextId !== undefined) { + publicationState = publicationStates.get(publicationContextId); + if (publicationState === undefined && publication) { + publicationContextId = String(++nextPublicationContextId); + publicationState = { active: true }; + publicationStates.set(publicationContextId, publicationState); + ownsPublicationState = true; + } else if (publicationState === undefined) { + publicationState = { active: false }; + } + } else if (publication) { + publicationContextId = String(++nextPublicationContextId); + publicationState = { active: true }; + publicationStates.set(publicationContextId, publicationState); + ownsPublicationState = true; + } else { + publicationState = { active: false }; + } + const token = { + publicationState, + publicationContextId, + scopeStack, + }; + const settlePublication = () => { + if (ownsPublicationState) { + publicationState.active = false; + publicationStates.delete(publicationContextId); + } + token.scopeStack = null; + }; const invoke = () => { Promise.resolve().then(() => ( next === undefined ? (spread ? fn(...arg0) : fn(arg0)) : (spread ? fn(...arg0, next) : fn(arg0, next)) )).then((value) => jsonValue(value === undefined ? null : value)).then((value) => { - token.publicationState.active = false; - token.scopeStack = null; + settlePublication(); resolve(value); }, (error) => { - token.publicationState.active = false; - token.scopeStack = null; + settlePublication(); let message = 'unknown error'; try { if (typeof error === 'string') { @@ -103,7 +144,17 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { }, promise(fn) { - return function __nemo_relay_promise_wrapper(error, arg0, spread, next, resolve, reject, publication, scopeStack) { + return function __nemo_relay_promise_wrapper( + error, + arg0, + spread, + next, + resolve, + reject, + publication, + publicationContextId, + scopeStack, + ) { if (error != null) { let message = 'unknown error'; try { @@ -112,7 +163,17 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { reject(message); return; } - callPromise(fn, arg0, spread, next, resolve, reject, publication, scopeStack); + callPromise( + fn, + arg0, + spread, + next, + resolve, + reject, + publication, + publicationContextId, + scopeStack, + ); }; }, @@ -120,6 +181,13 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { return eventSanitizerContext.getStore()?.publicationState.active === true; }, + eventSanitizerCallbackContextId() { + const current = eventSanitizerContext.getStore(); + return current?.publicationState.active === true + ? current.publicationContextId + : undefined; + }, + callbackScopeStack() { return eventSanitizerContext.getStore()?.scopeStack; }, @@ -129,7 +197,11 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { if (current === undefined) { return { active: false }; } - const token = { publicationState: current.publicationState, scopeStack }; + const token = { + publicationState: current.publicationState, + publicationContextId: current.publicationContextId, + scopeStack, + }; try { return { active: true, value: eventSanitizerContext.run(token, fn) }; } finally { @@ -201,6 +273,20 @@ pub(crate) fn event_sanitizer_callback_active(env: &Env) -> napi::Result { .get_value() } +pub(crate) fn event_sanitizer_callback_context_id(env: &Env) -> napi::Result> { + let factories = callback_factories(env)?; + let callback: JsFunction = factories.get_named_property("eventSanitizerCallbackContextId")?; + let value = callback.call::(None, &[])?; + if matches!(value.get_type()?, ValueType::Undefined | ValueType::Null) { + return Ok(None); + } + value + .coerce_to_string()? + .into_utf8()? + .into_owned() + .map(Some) +} + pub(crate) fn callback_scope_stack(env: &Env) -> napi::Result> { let factories = callback_factories(env)?; let callback: JsFunction = factories.get_named_property("callbackScopeStack")?; diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index b222edd53..94b27ecca 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -27,20 +27,22 @@ use crate::callback_factory; use crate::types::ScopeStack; tokio::task_local! { - static PUBLICATION_CALLBACK_ACTIVE: bool; + static PUBLICATION_CALLBACK_CONTEXT_ID: Option; } pub(crate) async fn with_publication_callback_context( - active: bool, + context_id: Option, future: F, ) -> F::Output { - PUBLICATION_CALLBACK_ACTIVE.scope(active, future).await + PUBLICATION_CALLBACK_CONTEXT_ID + .scope(context_id, future) + .await } -fn publication_callback_active() -> bool { - PUBLICATION_CALLBACK_ACTIVE - .try_with(|active| *active) - .unwrap_or(false) +fn publication_callback_context_id() -> Option { + PUBLICATION_CALLBACK_CONTEXT_ID + .try_with(Clone::clone) + .unwrap_or(None) } pub type JsonNextFn = @@ -75,6 +77,7 @@ struct CallArgs { spread: bool, next: Option, publication: bool, + publication_context_id: Option, /// Scope stack captured when Relay invokes the middleware. scope_stack: Option, completion: CallCompletion, @@ -152,7 +155,7 @@ fn build_next_unknown( env: &Env, next: NextFn, scope_stack: ScopeStackHandle, - publication: bool, + publication_context_id: Option, ) -> napi::Result { let next_fn = match next { NextFn::Json(next) => { @@ -160,9 +163,10 @@ fn build_next_unknown( let arg = ctx.get::(0).unwrap_or(Json::Null); let next = next.clone(); let scope_stack = scope_stack.clone(); + let publication_context_id = publication_context_id.clone(); ctx.env.execute_tokio_future( async move { - with_publication_callback_context(publication, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { next(arg) @@ -182,9 +186,10 @@ fn build_next_unknown( let arg = ctx.get::(0).unwrap_or(Json::Null); let next = next.clone(); let scope_stack = scope_stack.clone(); + let publication_context_id = publication_context_id.clone(); ctx.env.execute_tokio_future( async move { - with_publication_callback_context(publication, async move { + with_publication_callback_context(publication_context_id, async move { TASK_SCOPE_STACK .scope(scope_stack, async move { next(arg) @@ -261,7 +266,12 @@ impl PromiseAwareFn { "middleware next callback is missing its captured scope stack", ) })?; - build_next_unknown(&ctx.env, next, scope_stack, ctx.value.publication)? + build_next_unknown( + &ctx.env, + next, + scope_stack, + ctx.value.publication_context_id.clone(), + )? } None => undefined_to_unknown(&ctx.env)?, }; @@ -283,6 +293,10 @@ impl PromiseAwareFn { ctx.env.get_boolean(ctx.value.publication)?.raw(), ) }; + let publication_context_id = match ctx.value.publication_context_id { + Some(context_id) => json_to_unknown(&ctx.env, Json::String(context_id))?, + None => undefined_to_unknown(&ctx.env)?, + }; let scope_stack = match ctx.value.scope_stack { Some(scope_stack) => { let scope_stack = ScopeStack::from(scope_stack).into_instance(ctx.env)?; @@ -297,6 +311,7 @@ impl PromiseAwareFn { resolve, reject, publication, + publication_context_id, scope_stack, ]; Ok(args) @@ -418,7 +433,8 @@ impl PromiseAwareFn { arg0, spread: mode.spread, next, - publication: mode.publication || publication_callback_active(), + publication: mode.publication, + publication_context_id: publication_callback_context_id(), // Scope identity applies to every middleware callback. The // publication bit controls only re-entrant flush behavior. scope_stack: Some(current_scope_stack()), diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 6a104bc4e..1e62f1d70 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -422,6 +422,71 @@ describe('event sanitizer registries', () => { } }); + it('clears sanitizer re-entrancy after a native descendant round trip', async () => { + const events = capture('node-event-sanitize-native-descendant-sub'); + let secondSanitizerEntered; + const secondEntered = new Promise((resolve) => { + secondSanitizerEntered = resolve; + }); + let releaseSecondSanitizer; + const releaseSecond = new Promise((resolve) => { + releaseSecondSanitizer = resolve; + }); + let descendantFlushStarted; + const flushStarted = new Promise((resolve) => { + descendantFlushStarted = resolve; + }); + let descendantFlush; + let descendantExecution; + + lib.registerToolConditionalExecutionGuardrail( + 'node-event-native-descendant-conditional', + 0, + async (name) => { + if (name === 'node-event-native-descendant-tool') { + await secondEntered; + descendantFlush = lib.flushSubscribers(); + descendantFlushStarted(); + await descendantFlush; + } + return null; + }, + ); + lib.registerMarkSanitizeGuardrail( + 'node-event-native-descendant-sanitizer', + 0, + async (event, fields) => { + if (event.name === 'native-descendant-origin') { + descendantExecution = lib.toolCallExecute('node-event-native-descendant-tool', {}, (args) => args); + } else if (event.name === 'native-descendant-blocked') { + secondSanitizerEntered(); + await releaseSecond; + } + return fields; + }, + ); + try { + lib.event('native-descendant-origin', null, { raw: true }); + lib.event('native-descendant-blocked', null, { raw: true }); + await secondEntered; + await flushStarted; + const state = await Promise.race([ + descendantFlush.then(() => 'flushed'), + new Promise((resolve) => setTimeout(() => resolve('pending'), 50)), + ]); + assert.equal(state, 'pending'); + releaseSecondSanitizer(); + await descendantExecution; + await lib.flushSubscribers(); + await waitFor(events, 4); + } finally { + releaseSecondSanitizer(); + lib.deregisterMarkSanitizeGuardrail('node-event-native-descendant-sanitizer'); + lib.deregisterToolConditionalExecutionGuardrail('node-event-native-descendant-conditional'); + lib.deregisterSubscriber('node-event-sanitize-native-descendant-sub'); + } + }); + it('fails open and records invalid sanitizer results', async () => { const events = capture('node-event-sanitize-invalid-sub'); const invalidResults = { diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index 50ad75d3a..335e61fdf 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -583,6 +583,53 @@ describe('LLM guardrails', () => { } }); + it('stream response sanitizers preserve the invocation scope across await', async () => { + const originalStack = lib.currentScopeStack(); + const invocationStack = lib.createScopeStack(); + const unrelatedStack = lib.createScopeStack(); + const observed = []; + let invocationScope; + + registerLlmSanitizeResponseGuardrail('node_stream_response_scope', 10, async (response) => { + observed.push(lib.getHandle().uuid); + await new Promise((resolve) => setImmediate(resolve)); + observed.push(lib.getHandle().uuid); + return response; + }); + try { + const execution = lib.withScopeStack(invocationStack, () => { + invocationScope = lib.pushScope('stream-response-scope', lib.ScopeType.Agent); + return llmStreamCallExecute( + 'stream_response_scope', + makeNative(), + (wrapper) => { + lib.pushStreamChunk(wrapper.__nemo_relay_stream_id, { token: 'done' }); + lib.endStream(wrapper.__nemo_relay_stream_id); + }, + null, + () => ({ done: true }), + null, + null, + null, + null, + null, + null, + null, + null, + ); + }); + lib.setThreadScopeStack(unrelatedStack); + const stream = await execution; + assert.deepEqual(await stream.next(), { token: 'done' }); + assert.equal(await stream.next(), null); + assert.deepEqual(observed, [invocationScope.uuid, invocationScope.uuid]); + } finally { + lib.withScopeStack(invocationStack, () => lib.popScope(invocationScope)); + lib.setThreadScopeStack(originalStack); + deregisterLlmSanitizeResponseGuardrail('node_stream_response_scope'); + } + }); + it('stream response sanitizers can flush subscribers without deadlocking', async () => { let responseFlushed = false; registerSubscriber('node_stream_flush_subscriber', () => {}); diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index cbd6e3412..971975492 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -170,6 +170,7 @@ fn split_py_object_or_future_with_locals( py: Python<'_>, result: Py, task_locals: Option<&TaskLocals>, + invocation_context: Option<&Bound<'_, PyAny>>, ) -> FlowResult, PyValueFuture>> { let bound = result.bind(py); if bound.getattr("__await__").is_ok() { @@ -179,21 +180,31 @@ fn split_py_object_or_future_with_locals( pyo3_async_runtimes::into_future_with_locals(locals, result.into_bound(py)) .map_err(|e| FlowError::Internal(e.to_string()))?, ), - None => Box::pin(async move { - tokio::task::spawn_blocking(move || { - Python::attach(|py| { - let coroutine = py - .import("nemo_relay._event_sanitizer_context") - .and_then(|module| module.getattr("await_result")) - .and_then(|await_result| await_result.call1((result.bind(py),)))?; - py.import("asyncio") - .and_then(|asyncio| asyncio.call_method1("run", (coroutine,))) - .map(Bound::unbind) + None => { + let invocation_context = invocation_context.map(|context| context.clone().unbind()); + Box::pin(async move { + tokio::task::spawn_blocking(move || { + Python::attach(|py| { + let coroutine = py + .import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("await_result")) + .and_then(|await_result| await_result.call1((result.bind(py),)))?; + let asyncio_run = py + .import("asyncio") + .and_then(|asyncio| asyncio.getattr("run"))?; + match invocation_context { + Some(context) => context + .bind(py) + .call_method1("run", (asyncio_run, coroutine)) + .map(Bound::unbind), + None => asyncio_run.call1((coroutine,)).map(Bound::unbind), + } + }) }) + .await + .map_err(|error| PyRuntimeError::new_err(error.to_string()))? }) - .await - .map_err(|error| PyRuntimeError::new_err(error.to_string()))? - }), + } }; Ok(Err(future)) } else { @@ -259,12 +270,14 @@ fn task_locals_with_running_loop(registered: Option<&TaskLocals>) -> Option( py: Python<'py>, context: &PythonPublicationContext, + fallback_task_locals: Option, ) -> PyResult<(Bound<'py, PyAny>, Option)> { let invocation_context = context.context.bind(py).call_method0("copy")?; let task_locals = context .task_locals .clone() .and_then(running_task_locals) + .or(fallback_task_locals) .map(|locals| { TaskLocals::new(locals.event_loop(py)).with_context(invocation_context.clone()) }); @@ -572,9 +585,9 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context) + copy_publication_invocation(py, context, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; - (Some(context), publication_task_locals.or(task_locals)) + (Some(context), publication_task_locals) } None => (None, task_locals), }; @@ -597,7 +610,12 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { (None, false) => py_fn.bind(py).call1((name, py_args)), } .map_err(|e| FlowError::Internal(format!("Python tool callback failed: {e}")))?; - split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) })) .await?; Python::attach(|py| { @@ -622,7 +640,7 @@ pub fn wrap_py_tool_conditional_fn(py_fn: Py) -> ToolConditionalFn { let result = py_fn .call1(py, (name, py_args)) .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref(), None) })) .await?; Python::attach(|py| { @@ -1014,9 +1032,9 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context) + copy_publication_invocation(py, context, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; - (Some(context), publication_task_locals.or(task_locals)) + (Some(context), publication_task_locals) } None => (None, task_locals), }; @@ -1042,7 +1060,12 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest (None, false) => py_fn.bind(py).call1(args), } .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) })) .await?; Python::attach(|py| { @@ -1076,7 +1099,7 @@ pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { let result = py_fn .call1(py, (PyLLMRequest { inner: request },)) .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref(), None) })) .await?; Python::attach(|py| { @@ -1130,7 +1153,7 @@ pub fn wrap_py_llm_request_intercept_fn(py_fn: Py) -> LlmRequestIntercept FlowError::Internal(format!("LLM request intercept callable failed: {e}")) })?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref()) + split_py_object_or_future_with_locals(py, result, task_locals.as_ref(), None) })) .await?; Python::attach(|py| { @@ -1258,9 +1281,9 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context) + copy_publication_invocation(py, context, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; - (Some(context), publication_task_locals.or(task_locals)) + (Some(context), publication_task_locals) } None => (None, task_locals), }; @@ -1287,7 +1310,12 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon (None, false) => py_fn.bind(py).call1((py_response, py_context)), } .map_err(|error| FlowError::Internal(error.to_string()))?; - split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) })) .await?; Python::attach(|py| { @@ -1354,9 +1382,9 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context) + copy_publication_invocation(py, context, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; - (Some(context), publication_task_locals.or(task_locals)) + (Some(context), publication_task_locals) } None => (None, task_locals), }; @@ -1404,7 +1432,12 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { None => invoke.call1((py_fn.bind(py), py_event, py_fields)), } .map_err(|error| FlowError::Internal(error.to_string()))?; - split_py_object_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) }, ); let result = resolve_py_object_or_future(result).await?; diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 23e5bc520..00317c437 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -125,6 +125,28 @@ async def emit(name: str) -> None: assert observed == {"request-a": "request-a", "request-b": "request-b"} +async def test_async_mark_sanitizer_uses_cross_thread_emitter_context(capture_events): + request_id = contextvars.ContextVar("cross_thread_request_id", default="registration") + observed: list[str] = [] + + async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + observed.append(request_id.get()) + await asyncio.sleep(0) + observed.append(request_id.get()) + return fields + + guardrails.register_mark_sanitize("python-cross-thread-emitter-context", 0, sanitize) + token = request_id.set("emission") + try: + await asyncio.to_thread(scope.event, "cross-thread-emitter-context") + await subscribers.flush_async() + finally: + request_id.reset(token) + guardrails.deregister_mark_sanitize("python-cross-thread-emitter-context") + + assert observed == ["emission", "emission"] + + def test_sync_mark_sanitizer_uses_emitter_context(capture_events): request_id = contextvars.ContextVar("request_id", default="registration") observed: list[str] = [] @@ -194,9 +216,13 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve def test_async_sanitizer_registered_on_closed_loop_uses_fallback(capture_events): _capture_name, events = capture_events + request_id = contextvars.ContextVar("fallback_request_id", default="registration") + observed: list[str] = [] async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + observed.append(request_id.get()) await asyncio.sleep(0) + observed.append(request_id.get()) return { "data": {"fresh_loop": True}, "category_profile": fields["category_profile"], @@ -207,13 +233,16 @@ async def register() -> None: guardrails.register_mark_sanitize("python-closed-loop-fallback", 0, sanitize) asyncio.run(register()) + token = request_id.set("emission") try: scope.event("closed-loop-checkpoint", data={"raw": True}) subscribers.flush() finally: + request_id.reset(token) guardrails.deregister_mark_sanitize("python-closed-loop-fallback") assert events[-1].data == {"fresh_loop": True} + assert observed == ["emission", "emission"] @pytest.mark.parametrize("asynchronous", [False, True]) From 7981b0defb23698af3c69189a2eae19163212839 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 04:04:06 -0400 Subject: [PATCH 48/83] fix(python): preserve middleware invocation context Signed-off-by: Will Killian --- crates/python/src/py_callable.rs | 372 ++++++++++-------- .../coverage/py_callable_coverage_tests.rs | 2 +- python/nemo_relay/__init__.pyi | 20 +- python/tests/test_llm.py | 145 +++++++ python/tests/test_tools.py | 39 ++ 5 files changed, 406 insertions(+), 172 deletions(-) diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 971975492..764faddb1 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -43,7 +43,6 @@ use tokio_stream::wrappers::ReceiverStream; use nemo_relay::api::event::{Event, EventSanitizeFields}; use nemo_relay::api::llm::LlmRequest; -use nemo_relay::api::tool::ToolExecutionInterceptOutcome; use nemo_relay::codec::request::AnnotatedLlmRequest as AnnotatedLLMRequest; use nemo_relay::codec::response::AnnotatedLlmResponse as AnnotatedLLMResponse; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; @@ -151,21 +150,6 @@ async fn resolve_json_or_future( } } -fn split_py_object_or_future( - py: Python<'_>, - result: Py, -) -> FlowResult, PyValueFuture>> { - let bound = result.bind(py); - if bound.getattr("__await__").is_ok() { - reject_awaitable_from_sync_caller(bound)?; - let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) - .map_err(|error| FlowError::Internal(error.to_string()))?; - Ok(Err(Box::pin(future))) - } else { - Ok(Ok(result)) - } -} - fn split_py_object_or_future_with_locals( py: Python<'_>, result: Py, @@ -284,6 +268,24 @@ fn copy_publication_invocation<'py>( Ok((invocation_context, task_locals)) } +fn copy_middleware_invocation<'py>( + py: Python<'py>, + fallback_task_locals: Option, +) -> PyResult<(Option>, Option)> { + if let Some(context) = publication_context::() { + let (context, task_locals) = + copy_publication_invocation(py, &context, fallback_task_locals)?; + return Ok((Some(context), task_locals)); + } + let Some(locals) = fallback_task_locals else { + return Ok((None, None)); + }; + let invocation_context = locals.context(py).call_method0("copy")?; + let task_locals = + TaskLocals::new(locals.event_loop(py)).with_context(invocation_context.clone()); + Ok((Some(invocation_context), Some(task_locals))) +} + async fn resolve_py_object_or_future( outcome: FlowResult, PyValueFuture>>, ) -> FlowResult> { @@ -296,7 +298,15 @@ async fn resolve_py_object_or_future( fn next_async_iter_coro(async_iter: &Arc>) -> FlowResult>> { Python::attach(|py| { let iter = async_iter.bind(py); - match iter.call_method0("__anext__") { + let next = iter.getattr("__anext__"); + let result = + next.and_then( + |next| match pyo3_async_runtimes::tokio::get_current_locals(py) { + Ok(locals) => locals.context(py).call_method1("run", (next,)), + Err(_) => next.call0(), + }, + ); + match result { Ok(coro) => Ok(Some(coro.unbind())), Err(error) => { if error.is_instance_of::(py) { @@ -315,8 +325,10 @@ fn schedule_async_iter_task(coro: Py) -> FlowResult> { .and_then(|locals| { let kwargs = PyDict::new(py); kwargs.set_item("loop", locals.event_loop(py))?; - py.import("asyncio")? - .call_method("ensure_future", (coro,), Some(&kwargs)) + let ensure_future = py.import("asyncio")?.getattr("ensure_future")?; + locals + .context(py) + .call_method("run", (ensure_future, coro), Some(&kwargs)) }) .map(|task| task.unbind()) .map_err(|e| FlowError::Internal(e.to_string())) @@ -390,7 +402,15 @@ async fn await_async_iter_value(coro: Py) -> FlowResult> { async fn close_async_iter(async_iter: &Arc>) -> FlowResult<()> { let close = Python::attach(|py| { let iter = async_iter.bind(py); - match iter.call_method0("aclose") { + let close = iter.getattr("aclose"); + let result = + close.and_then( + |close| match pyo3_async_runtimes::tokio::get_current_locals(py) { + Ok(locals) => locals.context(py).call_method1("run", (close,)), + Err(_) => close.call0(), + }, + ); + match result { Ok(close) => Ok(Some(close.unbind())), Err(error) if error.is_instance_of::(py) => { Ok(None) @@ -547,12 +567,18 @@ impl LlmStreamInner for PythonAsyncIteratorStream { } } -fn stream_from_async_iter(async_iter: Py) -> FlowResult { +fn stream_from_async_iter( + async_iter: Py, + task_locals: Option, +) -> FlowResult { let (tx, rx) = tokio::sync::mpsc::channel::>(32); - let task_locals = Python::attach(|py| { - pyo3_async_runtimes::tokio::get_current_locals(py) - .map_err(|e: pyo3::PyErr| FlowError::Internal(e.to_string())) - })?; + let task_locals = match task_locals { + Some(locals) => locals, + None => Python::attach(|py| { + pyo3_async_runtimes::tokio::get_current_locals(py) + .map_err(|e: pyo3::PyErr| FlowError::Internal(e.to_string())) + })?, + }; let async_iter = Arc::new(async_iter); let (cancel, cancel_rx) = tokio::sync::watch::channel(false); @@ -589,7 +615,8 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => (None, task_locals), + None => copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?, }; let py_args = json_to_py(py, &args) .map_err(|e| FlowError::Internal(format!("tool json_to_py failed: {e}")))?; @@ -635,12 +662,21 @@ pub fn wrap_py_tool_conditional_fn(py_fn: Py) -> ToolConditionalFn { let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_args = json_to_py(py, &args).map_err(|e| FlowError::Internal(e.to_string()))?; - let result = py_fn - .call1(py, (name, py_args)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref(), None) + let result = match invocation_context.as_ref() { + Some(context) => context.call_method1("run", (py_fn.bind(py), name, py_args)), + None => py_fn.bind(py).call1((name, py_args)), + } + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) })) .await?; Python::attach(|py| { @@ -668,12 +704,16 @@ pub fn wrap_py_tool_request_intercept_fn(py_fn: Py) -> ToolInterceptFn { let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { resolve_json_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_args = json_to_py(py, &args).map_err(|e| FlowError::Internal(e.to_string()))?; - let result = py_fn - .call1(py, (name, py_args)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - split_json_or_future_with_locals(py, result, task_locals.as_ref()) + let result = match invocation_context.as_ref() { + Some(context) => context.call_method1("run", (py_fn.bind(py), name, py_args)), + None => py_fn.bind(py).call1((name, py_args)), + } + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_json_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) })) .await }) @@ -825,71 +865,47 @@ pub fn wrap_py_tool_exec_intercept_fn( py_fn: Py, ) -> nemo_relay::api::runtime::ToolExecutionFn { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new(move |name: &str, args: Json, next: ToolExecutionNextFn| { let py_fn = py_fn.clone(); let name = name.to_string(); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { - let outcome: FlowResult< - Result< - ToolExecutionInterceptOutcome, - Pin>> + Send>>, - >, - > = Python::attach(|py| { + let result = resolve_py_object_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_args = json_to_py(py, &args).map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; let py_next = PyToolNextFn { inner: next }; - let result = py_fn - .call1( - py, - ( - &name, - py_args, - py_next - .into_pyobject(py) - .map_err(|e| FlowError::Internal(e.to_string()))? - .into_any(), - ), - ) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - - let bound = result.bind(py); - if bound.getattr("__await__").is_ok() { - let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - Ok(Err(Box::pin(future) - as Pin< - Box>> + Send>, - >)) - } else { - let outcome = result - .extract::(py) - .map_err(|e| { - FlowError::Internal(format!( - "tool execution intercept must return ToolExecutionInterceptOutcome: {e}" - )) - })?; - Ok(Ok(outcome.inner)) + let py_next = py_next + .into_pyobject(py) + .map_err(|e| FlowError::Internal(e.to_string()))? + .into_any(); + let result = match invocation_context.as_ref() { + Some(context) => { + context.call_method1("run", (py_fn.bind(py), &name, py_args, py_next)) + } + None => py_fn.bind(py).call1((&name, py_args, py_next)), } - }); - - match outcome? { - Ok(json) => Ok(json), - Err(future) => { - let py_result = future - .await - .map_err(|e| FlowError::Internal(e.to_string()))?; - Python::attach(|py| { - py_result - .extract::(py) - .map(|value| value.inner) - .map_err(|e| { - FlowError::Internal(format!( - "tool execution intercept must return ToolExecutionInterceptOutcome: {e}" - )) - }) + .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) + })) + .await?; + Python::attach(|py| { + result + .extract::(py) + .map(|value| value.inner) + .map_err(|e| { + FlowError::Internal(format!( + "tool execution intercept must return ToolExecutionInterceptOutcome: {e}" + )) }) - } - } + }) }) }) } @@ -907,60 +923,46 @@ pub fn wrap_py_llm_exec_intercept_fn( + Sync, > { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new( move |name: &str, request: LlmRequest, next: LlmExecutionNextFn| { let py_fn = py_fn.clone(); let name = name.to_string(); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { - let outcome: FlowResult< - Result>> + Send>>>, - > = Python::attach(|py| { + let result = resolve_py_object_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = + copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_req = PyLLMRequest { inner: request }; let py_next = PyLlmNextFn { inner: next }; - let result = py_fn - .call1( - py, - ( - &name, - py_req - .into_pyobject(py) - .map_err(|e| FlowError::Internal(e.to_string()))? - .into_any(), - py_next - .into_pyobject(py) - .map_err(|e| FlowError::Internal(e.to_string()))? - .into_any(), - ), - ) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - - let bound = result.bind(py); - if bound.getattr("__await__").is_ok() { - let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - Ok(Err(Box::pin(future) - as Pin< - Box>> + Send>, - >)) - } else { - let json = py_to_json(bound) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - Ok(Ok(json)) - } - }); - - match outcome? { - Ok(json) => Ok(json), - Err(future) => { - let py_result = future - .await - .map_err(|e| FlowError::Internal(e.to_string()))?; - Python::attach(|py| { - py_to_json(py_result.bind(py)) - .map_err(|e: PyErr| FlowError::Internal(e.to_string())) - }) + let py_req = py_req + .into_pyobject(py) + .map_err(|e| FlowError::Internal(e.to_string()))? + .into_any(); + let py_next = py_next + .into_pyobject(py) + .map_err(|e| FlowError::Internal(e.to_string()))? + .into_any(); + let result = match invocation_context.as_ref() { + Some(context) => { + context.call_method1("run", (py_fn.bind(py), &name, py_req, py_next)) + } + None => py_fn.bind(py).call1((&name, py_req, py_next)), } - } + .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) + })) + .await?; + Python::attach(|py| { + py_to_json(result.bind(py)) + .map_err(|e: PyErr| FlowError::Internal(e.to_string())) + }) }) }, ) @@ -984,33 +986,44 @@ pub fn wrap_py_llm_stream_exec_intercept_fn( + Sync, > { let py_fn = Arc::new(py_fn); + let task_locals = capture_python_task_locals(); Arc::new( move |_name: &str, request: LlmRequest, next: LlmStreamExecutionNextFn| { let py_fn = py_fn.clone(); + let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { - let async_iter = resolve_py_object_or_future(Python::attach(|py| { + let (outcome, invocation_task_locals) = Python::attach(|py| { + let (invocation_context, task_locals) = + copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_req = PyLLMRequest { inner: request }; let py_next = PyLlmStreamNextFn { inner: next }; - let result = py_fn - .call1( - py, - ( - py_req - .into_pyobject(py) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))? - .into_any(), - py_next - .into_pyobject(py) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))? - .into_any(), - ), - ) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - split_py_object_or_future(py, result) - })) - .await?; + let py_req = py_req + .into_pyobject(py) + .map_err(|e: PyErr| FlowError::Internal(e.to_string()))? + .into_any(); + let py_next = py_next + .into_pyobject(py) + .map_err(|e: PyErr| FlowError::Internal(e.to_string()))? + .into_any(); + let result = match invocation_context.as_ref() { + Some(context) => { + context.call_method1("run", (py_fn.bind(py), py_req, py_next)) + } + None => py_fn.bind(py).call1((py_req, py_next)), + } + .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; + let outcome = split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + )?; + Ok::<_, FlowError>((outcome, task_locals)) + })?; + let async_iter = resolve_py_object_or_future(Ok(outcome)).await?; - stream_from_async_iter(async_iter) + stream_from_async_iter(async_iter, invocation_task_locals) }) }, ) @@ -1036,7 +1049,8 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => (None, task_locals), + None => copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?, }; let args = ( PyLLMRequest { inner: request }, @@ -1096,10 +1110,20 @@ pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { - let result = py_fn - .call1(py, (PyLLMRequest { inner: request },)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref(), None) + let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let request = PyLLMRequest { inner: request }; + let result = match invocation_context.as_ref() { + Some(context) => context.call_method1("run", (py_fn.bind(py), request)), + None => py_fn.bind(py).call1((request,)), + } + .map_err(|e| FlowError::Internal(e.to_string()))?; + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) })) .await?; Python::attach(|py| { @@ -1133,6 +1157,9 @@ pub fn wrap_py_llm_request_intercept_fn(py_fn: Py) -> LlmRequestIntercept let task_locals = task_locals_with_running_loop(task_locals.as_ref()); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = + copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_req = PyLLMRequest { inner: request }; let py_ann: Py = match annotated { Some(ann) => { @@ -1149,11 +1176,22 @@ pub fn wrap_py_llm_request_intercept_fn(py_fn: Py) -> LlmRequestIntercept } None => py.None(), }; - let result = py_fn.call1(py, (name, py_req, py_ann)).map_err(|e| { + let result = match invocation_context.as_ref() { + Some(context) => { + context.call_method1("run", (py_fn.bind(py), name, py_req, py_ann)) + } + None => py_fn.bind(py).call1((name, py_req, py_ann)), + } + .map_err(|e| { FlowError::Internal(format!("LLM request intercept callable failed: {e}")) })?; - split_py_object_or_future_with_locals(py, result, task_locals.as_ref(), None) + split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + ) })) .await?; Python::attach(|py| { @@ -1216,7 +1254,7 @@ pub fn wrap_py_llm_stream_exec_fn( .call1(py, (py_req,)) .map_err(|e: PyErr| FlowError::Internal(e.to_string())) })?; - stream_from_async_iter(async_iter) + stream_from_async_iter(async_iter, None) }) }) } @@ -1285,7 +1323,8 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => (None, task_locals), + None => copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?, }; let py_context = PyLlmSanitizeResponseContext { inner: context }; let py_response = json_to_py(py, &response) @@ -1386,7 +1425,8 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => (None, task_locals), + None => copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?, }; let py_event = match event.as_ref() { Event::Scope(inner) => Py::new( diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index d50760803..3f328fca7 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -539,7 +539,7 @@ async def coro_non_json(): .unwrap(); }); - let no_loop_err = match stream_from_async_iter(no_loop_iter) { + let no_loop_err = match stream_from_async_iter(no_loop_iter, None) { Ok(_) => panic!("expected missing event loop error"), Err(err) => err, }; diff --git a/python/nemo_relay/__init__.pyi b/python/nemo_relay/__init__.pyi index f640a2f72..396963fca 100644 --- a/python/nemo_relay/__init__.pyi +++ b/python/nemo_relay/__init__.pyi @@ -162,21 +162,31 @@ class EventSanitizeFields(TypedDict): metadata: Json | None ToolSanitizeGuardrail: TypeAlias = Callable[[str, Json], Json | Awaitable[Json]] +"""Guardrail callback that sanitizes emitted tool request or response payloads. + +Arguments: + The tool name and current JSON payload. + +Return: + JSON payload recorded on the emitted lifecycle event. + +Exceptional flow: + Exceptions fail open and preserve the last valid observability payload. +""" EventSanitizeGuardrail: TypeAlias = Callable[ [Event, EventSanitizeFields], EventSanitizeFields | Awaitable[EventSanitizeFields], ] -"""Guardrail callback that sanitizes emitted tool request or response payloads. +"""Guardrail callback that sanitizes emitted mark or scope event fields. Arguments: - The tool name and current JSON payload. + The immutable event snapshot and its mutable observability fields. Return: - JSON payload recorded on the emitted lifecycle event. + Observability fields recorded on the asynchronously published event. Exceptional flow: - Exceptions raised by the callback propagate through the lifecycle operation - that invoked the guardrail. + Exceptions fail open and preserve the last valid event snapshot. """ ToolConditionalExecutionGuardrail: TypeAlias = Callable[[str, Json], Optional[str] | Awaitable[Optional[str]]] """Guardrail callback that can block tool execution. diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index f3eb204b6..ffb079e4d 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -604,6 +604,151 @@ def test_deregister_nonexistent(self): class TestLLMInterceptsAsync: + async def test_sync_middleware_preserves_async_caller_context(self): + request_id = contextvars.ContextVar("llm_middleware_request_id", default="registration") + observed: list[tuple[str, str]] = [] + + def conditional(_request): + observed.append(("conditional", request_id.get())) + return None + + def request_intercept(_name, request, annotated): + observed.append(("request", request_id.get())) + return LLMRequestInterceptOutcome(request, annotated) + + def execution_intercept(_name, _request, _next): + observed.append(("execution", request_id.get())) + return {"ok": True} + + guardrails.register_llm_conditional_execution("py_llm_context_conditional", 1, conditional) + intercepts.register_llm_request("py_llm_context_request", 1, False, request_intercept) + intercepts.register_llm_execution("py_llm_context_execution", 1, execution_intercept) + token = request_id.set("emitter") + try: + assert await llm.execute("context_llm", make_request(), lambda _request: {}) == {"ok": True} + await llm.conditional_execution(make_request()) + standalone = await llm.request_intercepts("context_llm_standalone", make_request()) + assert standalone.request.content == make_request().content + finally: + request_id.reset(token) + intercepts.deregister_llm_execution("py_llm_context_execution") + intercepts.deregister_llm_request("py_llm_context_request") + guardrails.deregister_llm_conditional_execution("py_llm_context_conditional") + + assert observed == [ + ("conditional", "emitter"), + ("request", "emitter"), + ("execution", "emitter"), + ("conditional", "emitter"), + ("request", "emitter"), + ] + + async def test_sync_stream_intercept_preserves_async_caller_context(self): + request_id = contextvars.ContextVar("llm_stream_middleware_request_id", default="registration") + observed: list[tuple[str, str]] = [] + + def middleware(request, next): + observed.append(("callback", request_id.get())) + + async def generate(): + observed.append(("generator-before", request_id.get())) + await asyncio.sleep(0) + observed.append(("generator-after", request_id.get())) + upstream = await next(request) + async for chunk in upstream: + yield chunk + + return generate() + + def provider(_request): + async def generate(): + yield {"token": "ok"} + + return generate() + + intercepts.register_llm_stream_execution("py_llm_stream_context", 1, middleware) + token = request_id.set("emitter") + try: + stream = await llm.stream_execute( + "context_stream_llm", + make_request(), + provider, + lambda _chunk: None, + lambda: {}, + ) + assert [chunk async for chunk in stream] == [{"token": "ok"}] + finally: + request_id.reset(token) + intercepts.deregister_llm_stream_execution("py_llm_stream_context") + + assert observed == [ + ("callback", "emitter"), + ("generator-before", "emitter"), + ("generator-after", "emitter"), + ] + + async def test_custom_stream_iterator_methods_preserve_async_caller_context(self): + request_id = contextvars.ContextVar("llm_custom_iterator_request_id", default="registration") + observed: list[tuple[str, str]] = [] + + class CustomIterator: + def __aiter__(self): + return self + + def __anext__(self): + observed.append(("anext-sync", request_id.get())) + + async def step(): + observed.append(("anext-before", request_id.get())) + await asyncio.sleep(0) + observed.append(("anext-after", request_id.get())) + return {"token": "ok"} + + return step() + + def aclose(self): + observed.append(("aclose-sync", request_id.get())) + + async def close(): + observed.append(("aclose-before", request_id.get())) + await asyncio.sleep(0) + observed.append(("aclose-after", request_id.get())) + + return close() + + def middleware(_request, _next): + observed.append(("callback", request_id.get())) + return CustomIterator() + + intercepts.register_llm_stream_execution("py_llm_custom_iterator_context", 1, middleware) + token = request_id.set("emitter") + try: + stream = await llm.stream_execute( + "custom_iterator_context_llm", + make_request(), + lambda _request: None, + lambda _chunk: None, + lambda: {}, + ) + assert await anext(stream) == {"token": "ok"} + await stream.aclose() + finally: + request_id.reset(token) + intercepts.deregister_llm_stream_execution("py_llm_custom_iterator_context") + + assert observed[:4] == [ + ("callback", "emitter"), + ("anext-sync", "emitter"), + ("anext-before", "emitter"), + ("anext-after", "emitter"), + ] + assert [label for label, _value in observed[-3:]] == [ + "aclose-sync", + "aclose-before", + "aclose-after", + ] + assert all(value == "emitter" for _label, value in observed) + async def test_async_request_intercept_runs_on_originating_loop(self): originating_loop = asyncio.get_running_loop() diff --git a/python/tests/test_tools.py b/python/tests/test_tools.py index 37acf0434..2ad507903 100644 --- a/python/tests/test_tools.py +++ b/python/tests/test_tools.py @@ -4,6 +4,7 @@ """Tests for NeMo Relay tool lifecycle, guardrails, and intercepts.""" import asyncio +import contextvars from collections import UserDict, UserList from dataclasses import dataclass from typing import cast @@ -440,6 +441,44 @@ def test_request_intercept_raises_on_unserializable_return(self): class TestToolInterceptsAsync: + async def test_sync_middleware_preserves_async_caller_context(self): + request_id = contextvars.ContextVar("tool_middleware_request_id", default="registration") + observed: list[tuple[str, str]] = [] + + def conditional(_name, _args): + observed.append(("conditional", request_id.get())) + return None + + def request_intercept(_name, args): + observed.append(("request", request_id.get())) + return args + + def execution_intercept(_name, args, _next): + observed.append(("execution", request_id.get())) + return ToolExecutionInterceptOutcome(args) + + guardrails.register_tool_conditional_execution("py_tool_context_conditional", 1, conditional) + intercepts.register_tool_request("py_tool_context_request", 1, False, request_intercept) + intercepts.register_tool_execution("py_tool_context_execution", 1, execution_intercept) + token = request_id.set("emitter") + try: + assert await tools.execute("context_tool", {"ok": True}, lambda args: args) == {"ok": True} + await tools.conditional_execution("context_tool_standalone", {}) + assert await tools.request_intercepts("context_tool_standalone", {"ok": True}) == {"ok": True} + finally: + request_id.reset(token) + intercepts.deregister_tool_execution("py_tool_context_execution") + intercepts.deregister_tool_request("py_tool_context_request") + guardrails.deregister_tool_conditional_execution("py_tool_context_conditional") + + assert observed == [ + ("conditional", "emitter"), + ("request", "emitter"), + ("execution", "emitter"), + ("conditional", "emitter"), + ("request", "emitter"), + ] + async def test_async_request_intercept_runs_on_originating_loop(self): originating_loop = asyncio.get_running_loop() From 03879c1810ee5e86d84f691050b22bb7752af994 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 04:20:29 -0400 Subject: [PATCH 49/83] fix(python): cancel expired middleware contexts Signed-off-by: Will Killian --- crates/python/src/py_callable.rs | 90 +++++++++++++++---- python/nemo_relay/_event_sanitizer_context.py | 36 ++++++-- python/tests/test_event_sanitizers.py | 36 ++++++++ python/tests/test_llm.py | 82 +++++++++++++++++ python/tests/test_tools.py | 66 ++++++++++++++ 5 files changed, 287 insertions(+), 23 deletions(-) diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 764faddb1..6b3365e39 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -55,10 +55,80 @@ use crate::py_types::{ type PyValueFuture = Pin>> + Send>>; +struct CancellablePyFuture { + inner: PyValueFuture, + task: Option>, + task_locals: TaskLocals, +} + +impl Future for CancellablePyFuture { + type Output = PyResult>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + match this.inner.as_mut().poll(cx) { + Poll::Ready(result) => { + this.task.take(); + Poll::Ready(result) + } + Poll::Pending => Poll::Pending, + } + } +} + +impl Drop for CancellablePyFuture { + fn drop(&mut self) { + let Some(task) = self.task.take() else { + return; + }; + Python::attach(|py| { + let cancel = task.bind(py).getattr("cancel"); + if let Ok(cancel) = cancel { + let _ = self + .task_locals + .event_loop(py) + .call_method1("call_soon_threadsafe", (cancel,)); + } + }); + } +} + tokio::task_local! { pub(crate) static PY_AWAITABLES_ALLOWED: bool; } +fn schedule_python_awaitable( + py: Python<'_>, + awaitable: &Bound<'_, PyAny>, + task_locals: &TaskLocals, +) -> PyResult> { + let kwargs = PyDict::new(py); + kwargs.set_item("loop", task_locals.event_loop(py))?; + let ensure_future = py.import("asyncio")?.getattr("ensure_future")?; + task_locals + .context(py) + .call_method("run", (ensure_future, awaitable), Some(&kwargs)) + .map(Bound::unbind) +} + +fn cancellable_future_with_locals( + py: Python<'_>, + result: Py, + task_locals: &TaskLocals, +) -> FlowResult { + let task = schedule_python_awaitable(py, result.bind(py), task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let task_for_future = task.clone_ref(py); + let inner = + pyo3_async_runtimes::into_future_with_locals(task_locals, task_for_future.into_bound(py)) + .map_err(|error| FlowError::Internal(error.to_string()))?; + Ok(Box::pin(CancellablePyFuture { + inner: Box::pin(inner), + task: Some(task), + task_locals: task_locals.clone(), + })) +} + fn reject_awaitable_from_sync_caller(result: &Bound<'_, PyAny>) -> FlowResult<()> { if PY_AWAITABLES_ALLOWED .try_with(|allowed| *allowed) @@ -117,10 +187,7 @@ fn split_json_or_future_with_locals( if bound.getattr("__await__").is_ok() { reject_awaitable_from_sync_caller(bound)?; let future: PyValueFuture = match task_locals { - Some(locals) => Box::pin( - pyo3_async_runtimes::into_future_with_locals(locals, result.into_bound(py)) - .map_err(|error| FlowError::Internal(error.to_string()))?, - ), + Some(locals) => cancellable_future_with_locals(py, result, locals)?, None => Box::pin( pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) .map_err(|error| FlowError::Internal(error.to_string()))?, @@ -160,10 +227,7 @@ fn split_py_object_or_future_with_locals( if bound.getattr("__await__").is_ok() { reject_awaitable_from_sync_caller(bound)?; let future: PyValueFuture = match task_locals { - Some(locals) => Box::pin( - pyo3_async_runtimes::into_future_with_locals(locals, result.into_bound(py)) - .map_err(|e| FlowError::Internal(e.to_string()))?, - ), + Some(locals) => cancellable_future_with_locals(py, result, locals)?, None => { let invocation_context = invocation_context.map(|context| context.clone().unbind()); Box::pin(async move { @@ -322,15 +386,7 @@ fn next_async_iter_coro(async_iter: &Arc>) -> FlowResult) -> FlowResult> { Python::attach(|py| { pyo3_async_runtimes::tokio::get_current_locals(py) - .and_then(|locals| { - let kwargs = PyDict::new(py); - kwargs.set_item("loop", locals.event_loop(py))?; - let ensure_future = py.import("asyncio")?.getattr("ensure_future")?; - locals - .context(py) - .call_method("run", (ensure_future, coro), Some(&kwargs)) - }) - .map(|task| task.unbind()) + .and_then(|locals| schedule_python_awaitable(py, coro.bind(py), &locals)) .map_err(|e| FlowError::Internal(e.to_string())) }) } diff --git a/python/nemo_relay/_event_sanitizer_context.py b/python/nemo_relay/_event_sanitizer_context.py index 7991ebf4a..868cb35cf 100644 --- a/python/nemo_relay/_event_sanitizer_context.py +++ b/python/nemo_relay/_event_sanitizer_context.py @@ -10,19 +10,32 @@ from contextvars import ContextVar from typing import Any -_ACTIVE: ContextVar[bool] = ContextVar("nemo_relay_event_sanitizer_active", default=False) + +class _CallbackState: + """Shared liveness for contexts copied from one sanitizer invocation.""" + + __slots__ = ("active",) + + def __init__(self) -> None: + self.active = True + + +_ACTIVE: ContextVar[_CallbackState | None] = ContextVar("nemo_relay_event_sanitizer_active", default=None) def callback_active() -> bool: """Return whether the current Python context is running an event sanitizer.""" - return _ACTIVE.get() + state = _ACTIVE.get() + return state is not None and state.active -async def _await_result(result: Awaitable[Any]) -> Any: - token = _ACTIVE.set(True) +async def _await_result(result: Awaitable[Any], state: _CallbackState, owner: bool) -> Any: + token = _ACTIVE.set(state) try: return await result finally: + if owner: + state.active = False _ACTIVE.reset(token) @@ -33,11 +46,22 @@ async def await_result(result: Awaitable[Any]) -> Any: def invoke(callback: Callable[..., Any], *args: Any) -> Any: """Invoke a sanitizer while marking its sync and async execution contexts.""" - token = _ACTIVE.set(True) + state = _ACTIVE.get() + owner = state is None or not state.active + if owner: + state = _CallbackState() + assert state is not None + token = _ACTIVE.set(state) try: result = callback(*args) + except BaseException: + if owner: + state.active = False + raise finally: _ACTIVE.reset(token) if inspect.isawaitable(result): - return _await_result(result) + return _await_result(result, state, owner) + if owner: + state.active = False return result diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 00317c437..548b9e2ea 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -147,6 +147,42 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert observed == ["emission", "emission"] +async def test_sanitizer_descendants_lose_reentrant_flush_after_settlement(capture_events): + blocker_entered = asyncio.Event() + release_blocker = asyncio.Event() + descendant_finished = asyncio.Event() + descendant_task: asyncio.Task[None] | None = None + + async def descendant() -> None: + await blocker_entered.wait() + await subscribers.flush_async() + descendant_finished.set() + + async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + nonlocal descendant_task + if event.name == "descendant-origin": + descendant_task = asyncio.create_task(descendant()) + elif event.name == "descendant-blocked": + blocker_entered.set() + await release_blocker.wait() + return fields + + guardrails.register_mark_sanitize("python-descendant-flush-liveness", 0, sanitize) + try: + scope.event("descendant-origin") + scope.event("descendant-blocked") + await asyncio.wait_for(blocker_entered.wait(), timeout=1) + await asyncio.sleep(0.05) + assert not descendant_finished.is_set() + release_blocker.set() + await asyncio.wait_for(subscribers.flush_async(), timeout=1) + assert descendant_task is not None + await asyncio.wait_for(descendant_task, timeout=1) + finally: + release_blocker.set() + guardrails.deregister_mark_sanitize("python-descendant-flush-liveness") + + def test_sync_mark_sanitizer_uses_emitter_context(capture_events): request_id = contextvars.ContextVar("request_id", default="registration") observed: list[str] = [] diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index ffb079e4d..fbde382e3 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -604,6 +604,88 @@ def test_deregister_nonexistent(self): class TestLLMInterceptsAsync: + async def test_cancelling_execute_cancels_pending_execution_intercept(self): + started = asyncio.Event() + release = asyncio.Event() + cancelled = asyncio.Event() + provider_calls: list[LLMRequest] = [] + + async def middleware(_name, request, next): + started.set() + try: + await release.wait() + return await next(request) + except asyncio.CancelledError: + cancelled.set() + raise + + def provider(request): + provider_calls.append(request) + return {"ok": True} + + intercepts.register_llm_execution("py_llm_cancel_intercept", 1, middleware) + try: + execution = asyncio.ensure_future(llm.execute("cancel_llm", make_request(), provider)) + await asyncio.wait_for(started.wait(), timeout=1) + execution.cancel() + with pytest.raises(asyncio.CancelledError): + await execution + await asyncio.wait_for(cancelled.wait(), timeout=1) + release.set() + await asyncio.sleep(0) + finally: + release.set() + intercepts.deregister_llm_execution("py_llm_cancel_intercept") + + assert provider_calls == [] + + async def test_cancelling_stream_execute_cancels_pending_stream_intercept(self): + started = asyncio.Event() + release = asyncio.Event() + cancelled = asyncio.Event() + provider_calls: list[LLMRequest] = [] + + async def middleware(request, next): + started.set() + try: + await release.wait() + return await next(request) + except asyncio.CancelledError: + cancelled.set() + raise + + def provider(request): + provider_calls.append(request) + + async def generate(): + yield {"token": "unexpected"} + + return generate() + + intercepts.register_llm_stream_execution("py_llm_stream_cancel_intercept", 1, middleware) + try: + execution = asyncio.ensure_future( + llm.stream_execute( + "cancel_stream_llm", + make_request(), + provider, + lambda _chunk: None, + lambda: {}, + ) + ) + await asyncio.wait_for(started.wait(), timeout=1) + execution.cancel() + with pytest.raises(asyncio.CancelledError): + await execution + await asyncio.wait_for(cancelled.wait(), timeout=1) + release.set() + await asyncio.sleep(0) + finally: + release.set() + intercepts.deregister_llm_stream_execution("py_llm_stream_cancel_intercept") + + assert provider_calls == [] + async def test_sync_middleware_preserves_async_caller_context(self): request_id = contextvars.ContextVar("llm_middleware_request_id", default="registration") observed: list[tuple[str, str]] = [] diff --git a/python/tests/test_tools.py b/python/tests/test_tools.py index 2ad507903..b1f034d43 100644 --- a/python/tests/test_tools.py +++ b/python/tests/test_tools.py @@ -441,6 +441,72 @@ def test_request_intercept_raises_on_unserializable_return(self): class TestToolInterceptsAsync: + async def test_cancelling_execute_cancels_pending_request_intercept(self): + started = asyncio.Event() + cancelled = asyncio.Event() + provider_calls: list[dict] = [] + + async def request_intercept(_name, args): + started.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + cancelled.set() + raise + return args + + def provider(args): + provider_calls.append(args) + return args + + intercepts.register_tool_request("py_tool_cancel_request", 1, False, request_intercept) + try: + execution = asyncio.ensure_future(tools.execute("cancel_request_tool", {}, provider)) + await asyncio.wait_for(started.wait(), timeout=1) + execution.cancel() + with pytest.raises(asyncio.CancelledError): + await execution + await asyncio.wait_for(cancelled.wait(), timeout=1) + finally: + intercepts.deregister_tool_request("py_tool_cancel_request") + + assert provider_calls == [] + + async def test_cancelling_execute_cancels_pending_execution_intercept(self): + started = asyncio.Event() + release = asyncio.Event() + cancelled = asyncio.Event() + provider_calls: list[dict] = [] + + async def middleware(_name, args, next): + started.set() + try: + await release.wait() + return ToolExecutionInterceptOutcome(await next(args)) + except asyncio.CancelledError: + cancelled.set() + raise + + def provider(args): + provider_calls.append(args) + return args + + intercepts.register_tool_execution("py_tool_cancel_intercept", 1, middleware) + try: + execution = asyncio.ensure_future(tools.execute("cancel_tool", {"ok": True}, provider)) + await asyncio.wait_for(started.wait(), timeout=1) + execution.cancel() + with pytest.raises(asyncio.CancelledError): + await execution + await asyncio.wait_for(cancelled.wait(), timeout=1) + release.set() + await asyncio.sleep(0) + finally: + release.set() + intercepts.deregister_tool_execution("py_tool_cancel_intercept") + + assert provider_calls == [] + async def test_sync_middleware_preserves_async_caller_context(self): request_id = contextvars.ContextVar("tool_middleware_request_id", default="registration") observed: list[tuple[str, str]] = [] From 9e0668ba5adc5d4120f518b617a8dfc733ce8ea8 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 05:31:26 -0400 Subject: [PATCH 50/83] fix: make async middleware cancellation safe Signed-off-by: Will Killian --- ATTRIBUTIONS-Rust.md | 37 +- crates/core/src/api/llm.rs | 127 +++++ crates/core/src/api/runtime/state.rs | 52 +- crates/core/src/api/tool.rs | 43 +- .../tests/integration/middleware_tests.rs | 169 +++++++ crates/python/src/py_callable.rs | 466 +++++++++++------- crates/python/src/test_support.rs | 37 +- .../nemo_guardrails_coverage_tests.rs | 17 + .../coverage/py_callable_coverage_tests.rs | 32 +- python/nemo_relay/_event_sanitizer_context.py | 24 + python/nemo_relay/tools.py | 8 +- python/tests/test_event_sanitizers.py | 29 ++ python/tests/test_llm.py | 23 +- python/tests/test_tools.py | 70 ++- 14 files changed, 900 insertions(+), 234 deletions(-) diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index ee60e8a33..a9df3ef75 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -26115,9 +26115,8 @@ SOFTWARE. ## md-5 - 0.11.0 **Repository URL**: https://github.com/RustCrypto/hashes -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -26322,38 +26321,6 @@ See the License for the specific language governing permissions and limitations under the License. ``` -### License File: LICENSE-MIT -``` -Copyright (c) 2016-2026 The RustCrypto Project Developers -Copyright (c) 2016 Artyom Pavlov -Copyright (c) 2009-2013 Mozilla Foundation -Copyright (c) 2006-2009 Graydon Hoare - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -``` - ## memchr - 2.8.0 **Repository URL**: https://github.com/BurntSushi/memchr **License Type(s)**: MIT diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index b6d68fcb6..514eca4f8 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -1203,6 +1203,117 @@ async fn emit_llm_end_without_output( Ok(()) } +struct ManagedLlmCompletion { + handle: Option, + metadata: Option, + response_codec: Option>, + subscribers: Vec, +} + +impl ManagedLlmCompletion { + fn new( + handle: &LlmHandle, + metadata: Option, + response_codec: Option>, + subscribers: &[EventSubscriberFn], + ) -> Self { + Self { + handle: Some(handle.clone()), + metadata, + response_codec, + subscribers: subscribers.to_vec(), + } + } + + fn disarm(&mut self) { + self.handle = None; + } +} + +impl Drop for ManagedLlmCompletion { + fn drop(&mut self) { + let Some(handle) = self.handle.take() else { + return; + }; + let metadata = metadata_with_otel_status( + self.metadata.take(), + "ERROR", + Some("LLM execution cancelled".into()), + ); + let scope_stack = handle.captured_scope_stack().clone(); + let entries = match scope_stack.read() { + Ok(scope_guard) => { + let scope_locals = scope_guard.collect_scope_local_registries(|registries| { + ®istries.llm_sanitize_response_guardrails + }); + global_context() + .read() + .map(|state| state.llm_sanitize_response_entries(&scope_locals)) + .unwrap_or_default() + } + Err(_) => Vec::new(), + }; + handle + .optimization_recorder + .close_for_finalization(Some("execution_cancelled")); + enqueue_optimization_marks(&handle, &self.subscribers); + let event = global_context() + .read() + .ok() + .map(|state| state.end_llm_handle(&handle, None, metadata.clone(), None)); + let Some(event) = event else { + return; + }; + let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); + let response_codec = self.response_codec.take(); + let subscribers = std::mem::take(&mut self.subscribers); + let fallback_data = handle.data.clone(); + dispatch_transformed_event( + event, + Box::new(move |event| { + Box::pin(async move { + let Some(data) = fallback_data else { + return event; + }; + let data = NemoRelayContextState::llm_sanitize_response_snapshot_chain( + data, + LlmSanitizeResponseContext::for_response_codec(response_codec), + &entries, + ) + .await; + let annotation_omitted = data.as_ref().is_none_or(Json::is_null); + let annotated_response = (!annotation_omitted) + .then(|| { + let pricing = crate::codec::response::active_pricing_resolver(); + finalize_optimization_summary( + &handle.optimization_recorder, + None, + handle.model_name.as_deref(), + &pricing, + ) + }) + .flatten() + .map(|summary| { + Arc::new(AnnotatedLlmResponse { + optimization_summary: Some(summary), + ..AnnotatedLlmResponse::default() + }) + }); + global_context() + .read() + .map(|state| { + state.end_llm_handle(&handle, data, metadata, annotated_response) + }) + .unwrap_or(event) + }) + }), + event_sanitizers, + &subscribers, + scope_stack, + ); + } +} + /// Execute an LLM call through the managed middleware pipeline. /// /// This runs conditional-execution guardrails, request intercepts, and @@ -1348,6 +1459,12 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { .record_all(optimization_contributions); emit_optimization_marks(&handle, &lifecycle_subscribers).await; + let mut completion = ManagedLlmCompletion::new( + &handle, + metadata.clone(), + response_codec.clone(), + &lifecycle_subscribers, + ); let execution_name = name.clone(); let event_uuid = handle.uuid; let execution = with_active_event_uuid( @@ -1387,6 +1504,7 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { Some(&lifecycle_subscribers), ) .await?; + completion.disarm(); Ok(response) } Err(error) => { @@ -1399,6 +1517,7 @@ pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result { Some(&lifecycle_subscribers), ) .await; + completion.disarm(); Err(error) } } @@ -1550,6 +1669,12 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu .record_all(optimization_contributions); emit_optimization_marks(&handle, &lifecycle_subscribers).await; + let mut completion = ManagedLlmCompletion::new( + &handle, + metadata.clone(), + response_codec.clone(), + &lifecycle_subscribers, + ); let execution_name = name.clone(); let event_uuid = handle.uuid; let execution = with_active_event_uuid( @@ -1583,6 +1708,7 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu response_codec, lifecycle_subscribers, ); + completion.disarm(); Ok(LlmJsonStream::from_closeable(wrapper)) } Err(error) => { @@ -1595,6 +1721,7 @@ pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Resu Some(&lifecycle_subscribers), ) .await; + completion.disarm(); Err(error) } } diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index 2ed3a9c79..ebfc8b112 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -49,6 +49,42 @@ use chrono::{Duration, Utc}; use serde_json::json; use uuid::Uuid; +struct GuardrailScopeCompletion<'a> { + handle: Option, + subscribers: &'a [EventSubscriberFn], +} + +impl GuardrailScopeCompletion<'_> { + fn new(handle: ScopeHandle, subscribers: &[EventSubscriberFn]) -> GuardrailScopeCompletion<'_> { + GuardrailScopeCompletion { + handle: Some(handle), + subscribers, + } + } + + fn finish(mut self, output: Json) { + let handle = self.handle.take().expect("guardrail scope handle"); + NemoRelayContextState::emit_guardrail_scope_end(&handle, output, self.subscribers); + } +} + +impl Drop for GuardrailScopeCompletion<'_> { + fn drop(&mut self) { + let Some(handle) = self.handle.take() else { + return; + }; + NemoRelayContextState::emit_guardrail_scope_end( + &handle, + json!({ + "allowed": false, + "cancelled": true, + "error": "guardrail evaluation cancelled", + }), + self.subscribers, + ); + } +} + /// Process-global runtime state backing middleware and event emission. /// /// The public API layer stores one shared instance of this type for the @@ -560,7 +596,7 @@ impl NemoRelayContextState { )) } - async fn emit_guardrail_scope_start( + fn emit_guardrail_scope_start( name: &str, parent_uuid: Option, metadata: Option, @@ -598,7 +634,7 @@ impl NemoRelayContextState { handle } - async fn emit_guardrail_scope_end( + fn emit_guardrail_scope_end( handle: &ScopeHandle, output: Json, subscribers: &[EventSubscriberFn], @@ -860,8 +896,8 @@ impl NemoRelayContextState { "target_name": name, }), subscribers, - ) - .await; + ); + let completion = GuardrailScopeCompletion::new(handle, subscribers); let callback = Arc::clone(&entry.payload); let callback_name = name.to_string(); let callback_args = args.clone(); @@ -891,7 +927,7 @@ impl NemoRelayContextState { "error": error.to_string(), }), }; - Self::emit_guardrail_scope_end(&handle, output, subscribers).await; + completion.finish(output); if let Some(error) = result? { return Ok(Some(error)); } @@ -1237,8 +1273,8 @@ impl NemoRelayContextState { "kind": "llm_conditional_execution", }), subscribers, - ) - .await; + ); + let completion = GuardrailScopeCompletion::new(handle, subscribers); let callback = Arc::clone(&entry.payload); let callback_request = request.clone(); let result = match AssertUnwindSafe(async move { callback(callback_request).await }) @@ -1266,7 +1302,7 @@ impl NemoRelayContextState { "error": error.to_string(), }), }; - Self::emit_guardrail_scope_end(&handle, output, subscribers).await; + completion.finish(output); if let Some(error) = result? { return Ok(Some(error)); } diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 719f2bb58..5333928c4 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -562,7 +562,7 @@ async fn tool_call_end_with_pending_marks( Ok(()) } -async fn emit_tool_end_without_output( +fn emit_tool_end_without_output( handle: &ToolHandle, metadata: Option, lifecycle_subscribers: &[EventSubscriberFn], @@ -579,6 +579,40 @@ async fn emit_tool_end_without_output( Ok(()) } +struct ManagedToolCompletion { + handle: Option, + metadata: Option, + subscribers: Vec, +} + +impl ManagedToolCompletion { + fn new(handle: &ToolHandle, metadata: Option, subscribers: &[EventSubscriberFn]) -> Self { + Self { + handle: Some(handle.clone()), + metadata, + subscribers: subscribers.to_vec(), + } + } + + fn disarm(&mut self) { + self.handle = None; + } +} + +impl Drop for ManagedToolCompletion { + fn drop(&mut self) { + let Some(handle) = self.handle.take() else { + return; + }; + let metadata = metadata_with_otel_status( + self.metadata.take(), + "ERROR", + Some("tool execution cancelled".into()), + ); + let _ = emit_tool_end_without_output(&handle, metadata, &self.subscribers); + } +} + /// Execute a tool call through the managed middleware pipeline. /// /// This runs conditional-execution guardrails, request intercepts, @@ -708,6 +742,8 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { state.tool_build_execution_chain(&name, func, &scope_locals) }; + let mut completion = + ManagedToolCompletion::new(&handle, metadata.clone(), &lifecycle_subscribers); match with_active_event_uuid(handle.uuid, execution(intercepted_args)).await { Ok(outcome) => { let ToolExecutionInterceptOutcome { @@ -726,13 +762,14 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { Some(&lifecycle_subscribers), ) .await?; + completion.disarm(); Ok(result) } Err(error) => { let end_metadata = metadata_with_otel_status(metadata, "ERROR", Some(error.to_string())); - let _ = - emit_tool_end_without_output(&handle, end_metadata, &lifecycle_subscribers).await; + let _ = emit_tool_end_without_output(&handle, end_metadata, &lifecycle_subscribers); + completion.disarm(); Err(error) } } diff --git a/crates/core/tests/integration/middleware_tests.rs b/crates/core/tests/integration/middleware_tests.rs index d5225bbfa..053cba446 100644 --- a/crates/core/tests/integration/middleware_tests.rs +++ b/crates/core/tests/integration/middleware_tests.rs @@ -1321,6 +1321,175 @@ async fn test_repeated_next_marks_follow_invocation_order_not_completion_order() deregister_subscriber("tool_concurrent_next_observer").unwrap(); } +#[tokio::test] +async fn dropping_pending_tool_execution_closes_the_managed_lifecycle() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = events.clone(); + register_subscriber( + "cancelled_tool_lifecycle", + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .unwrap(); + + let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); + let entered_tx = Arc::new(Mutex::new(Some(entered_tx))); + register_tool_execution_intercept( + "pending_tool_execution", + 1, + Arc::new(move |_name, _args, _next| { + if let Some(sender) = entered_tx.lock().unwrap().take() { + let _ = sender.send(()); + } + Box::pin(std::future::pending()) + }), + ) + .unwrap(); + + let mut execution = Box::pin(tool_call_execute( + nemo_relay::api::tool::ToolCallExecuteParams::builder() + .name("cancelled-tool") + .args(json!({})) + .func(Arc::new(|args| Box::pin(async move { Ok(args) }))) + .build(), + )); + tokio::select! { + result = &mut execution => panic!("execution unexpectedly completed: {result:?}"), + result = entered_rx => result.unwrap(), + } + drop(execution); + flush_subscribers().unwrap(); + + let lifecycle = events + .lock() + .unwrap() + .iter() + .filter(|event| event.name() == "cancelled-tool") + .filter_map(Event::scope_category) + .collect::>(); + assert_eq!(lifecycle, [ScopeCategory::Start, ScopeCategory::End]); + + deregister_tool_execution_intercept("pending_tool_execution").unwrap(); + deregister_subscriber("cancelled_tool_lifecycle").unwrap(); +} + +#[tokio::test] +async fn dropping_pending_conditional_closes_the_guardrail_scope() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = events.clone(); + register_subscriber( + "cancelled_guardrail_lifecycle", + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .unwrap(); + + let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); + let entered_tx = Arc::new(Mutex::new(Some(entered_tx))); + register_tool_conditional_execution_guardrail( + "pending_conditional", + 1, + Arc::new(move |_name, _args| { + if let Some(sender) = entered_tx.lock().unwrap().take() { + let _ = sender.send(()); + } + Box::pin(std::future::pending()) + }), + ) + .unwrap(); + + let mut execution = Box::pin(tool_call_execute( + nemo_relay::api::tool::ToolCallExecuteParams::builder() + .name("cancelled-conditional-tool") + .args(json!({})) + .func(Arc::new(|args| Box::pin(async move { Ok(args) }))) + .build(), + )); + tokio::select! { + result = &mut execution => panic!("execution unexpectedly completed: {result:?}"), + result = entered_rx => result.unwrap(), + } + drop(execution); + flush_subscribers().unwrap(); + + let lifecycle = events + .lock() + .unwrap() + .iter() + .filter(|event| event.name() == "pending_conditional") + .filter_map(Event::scope_category) + .collect::>(); + assert_eq!(lifecycle, [ScopeCategory::Start, ScopeCategory::End]); + + deregister_tool_conditional_execution_guardrail("pending_conditional").unwrap(); + deregister_subscriber("cancelled_guardrail_lifecycle").unwrap(); +} + +#[tokio::test] +async fn dropping_pending_llm_execution_closes_the_managed_lifecycle() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = events.clone(); + register_subscriber( + "cancelled_llm_lifecycle", + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .unwrap(); + + let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); + let entered_tx = Arc::new(Mutex::new(Some(entered_tx))); + register_llm_execution_intercept( + "pending_llm_execution", + 1, + Arc::new(move |_name, _request, _next| { + if let Some(sender) = entered_tx.lock().unwrap().take() { + let _ = sender.send(()); + } + Box::pin(std::future::pending()) + }), + ) + .unwrap(); + + let mut execution = Box::pin(llm_call_execute( + LlmCallExecuteParams::builder() + .name("cancelled-llm") + .request(LlmRequest { + headers: serde_json::Map::new(), + content: json!({"model": "test"}), + }) + .data(json!({"fallback": true})) + .func(Arc::new(|_request| Box::pin(async { Ok(json!({})) }))) + .build(), + )); + tokio::select! { + result = &mut execution => panic!("execution unexpectedly completed: {result:?}"), + result = entered_rx => result.unwrap(), + } + drop(execution); + flush_subscribers().unwrap(); + + let lifecycle = events + .lock() + .unwrap() + .iter() + .filter(|event| event.name() == "cancelled-llm") + .filter_map(Event::scope_category) + .collect::>(); + assert_eq!(lifecycle, [ScopeCategory::Start, ScopeCategory::End]); + + deregister_llm_execution_intercept("pending_llm_execution").unwrap(); + deregister_subscriber("cancelled_llm_lifecycle").unwrap(); +} + // ========================================================================= // Guardrail Conditional Execution Tests // ========================================================================= diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 6b3365e39..e193a8b42 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -22,7 +22,7 @@ use std::future::Future; use std::pin::Pin; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use nemo_relay::api::runtime::subscriber_dispatcher::{PublicationContext, publication_context}; @@ -57,8 +57,7 @@ type PyValueFuture = Pin>> + Send>>; struct CancellablePyFuture { inner: PyValueFuture, - task: Option>, - task_locals: TaskLocals, + scheduled: Arc>, } impl Future for CancellablePyFuture { @@ -68,7 +67,7 @@ impl Future for CancellablePyFuture { let this = self.get_mut(); match this.inner.as_mut().poll(cx) { Poll::Ready(result) => { - this.task.take(); + this.scheduled.lock().expect("scheduled awaitable").task = None; Poll::Ready(result) } Poll::Pending => Poll::Pending, @@ -78,17 +77,75 @@ impl Future for CancellablePyFuture { impl Drop for CancellablePyFuture { fn drop(&mut self) { - let Some(task) = self.task.take() else { - return; + let task = { + let mut scheduled = self.scheduled.lock().expect("scheduled awaitable"); + scheduled.cancelled = true; + scheduled.task.take() }; + let Some(task) = task else { return }; Python::attach(|py| { - let cancel = task.bind(py).getattr("cancel"); - if let Ok(cancel) = cancel { - let _ = self - .task_locals - .event_loop(py) - .call_method1("call_soon_threadsafe", (cancel,)); + let _ = cancel_python_task(py, &task, &self.scheduled); + }); + } +} + +struct ScheduledAwaitable { + task: Option>, + task_locals: TaskLocals, + cancelled: bool, +} + +#[pyclass] +struct SchedulePythonAwaitable { + awaitable: Option>, + sender: Option>>>, + scheduled: Arc>, +} + +#[pymethods] +impl SchedulePythonAwaitable { + fn __call__(&mut self, py: Python<'_>) { + let result = match self.awaitable.take() { + Some(awaitable) => { + let result = py + .import("asyncio") + .and_then(|asyncio| asyncio.getattr("ensure_future")) + .and_then(|ensure_future| ensure_future.call1((awaitable.bind(py),))) + .map(Bound::unbind); + if result.is_err() { + let _ = awaitable.bind(py).call_method0("close"); + } + result } + None => Err(PyRuntimeError::new_err( + "Python awaitable was already scheduled", + )), + }; + + if let Ok(task) = &result { + let cancelled = { + let mut scheduled = self.scheduled.lock().expect("scheduled awaitable"); + scheduled.task = Some(task.clone_ref(py)); + scheduled.cancelled + }; + if cancelled { + let _ = task.bind(py).call_method0("cancel"); + } + } + + if let Some(sender) = self.sender.take() { + let _ = sender.send(result); + } + } +} + +impl Drop for SchedulePythonAwaitable { + fn drop(&mut self) { + let Some(awaitable) = self.awaitable.take() else { + return; + }; + Python::attach(|py| { + let _ = awaitable.bind(py).call_method0("close"); }); } } @@ -99,16 +156,56 @@ tokio::task_local! { fn schedule_python_awaitable( py: Python<'_>, - awaitable: &Bound<'_, PyAny>, + awaitable: Py, task_locals: &TaskLocals, -) -> PyResult> { +) -> PyResult<( + tokio::sync::oneshot::Receiver>>, + Arc>, +)> { + let (sender, receiver) = tokio::sync::oneshot::channel(); + let scheduled = Arc::new(Mutex::new(ScheduledAwaitable { + task: None, + task_locals: task_locals.clone(), + cancelled: false, + })); let kwargs = PyDict::new(py); - kwargs.set_item("loop", task_locals.event_loop(py))?; - let ensure_future = py.import("asyncio")?.getattr("ensure_future")?; - task_locals - .context(py) - .call_method("run", (ensure_future, awaitable), Some(&kwargs)) - .map(Bound::unbind) + kwargs.set_item("context", task_locals.context(py))?; + task_locals.event_loop(py).call_method( + "call_soon_threadsafe", + (SchedulePythonAwaitable { + awaitable: Some(awaitable), + sender: Some(sender), + scheduled: scheduled.clone(), + },), + Some(&kwargs), + )?; + Ok((receiver, scheduled)) +} + +fn cancel_python_task( + py: Python<'_>, + task: &Py, + scheduled: &Arc>, +) -> PyResult<()> { + let event_loop = { + let scheduled = scheduled.lock().expect("scheduled awaitable"); + scheduled.task_locals.event_loop(py) + }; + if event_loop.call_method0("is_closed")?.is_truthy()? { + return Ok(()); + } + let cancel = task.bind(py).getattr("cancel")?; + let on_event_loop = py + .import("asyncio")? + .getattr("get_running_loop")? + .call0() + .is_ok_and(|running_loop| running_loop.is(&event_loop)); + if on_event_loop { + cancel.call0()?; + } else { + event_loop.call_method1("call_soon_threadsafe", (cancel,))?; + } + Ok(()) } fn cancellable_future_with_locals( @@ -116,16 +213,21 @@ fn cancellable_future_with_locals( result: Py, task_locals: &TaskLocals, ) -> FlowResult { - let task = schedule_python_awaitable(py, result.bind(py), task_locals) + let (receiver, scheduled) = schedule_python_awaitable(py, result, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; - let task_for_future = task.clone_ref(py); - let inner = - pyo3_async_runtimes::into_future_with_locals(task_locals, task_for_future.into_bound(py)) - .map_err(|error| FlowError::Internal(error.to_string()))?; + let task_locals = task_locals.clone(); + let inner = async move { + let task = receiver + .await + .map_err(|_| PyRuntimeError::new_err("Python awaitable scheduling was cancelled"))??; + Python::attach(|py| { + pyo3_async_runtimes::into_future_with_locals(&task_locals, task.into_bound(py)) + })? + .await + }; Ok(Box::pin(CancellablePyFuture { inner: Box::pin(inner), - task: Some(task), - task_locals: task_locals.clone(), + scheduled, })) } @@ -162,22 +264,6 @@ fn validate_python_llm_sanitizer_signature(py_fn: &Py) -> PyResult<()> { }) } -fn split_json_or_future( - py: Python<'_>, - result: Py, -) -> FlowResult> { - let bound = result.bind(py); - if bound.getattr("__await__").is_ok() { - reject_awaitable_from_sync_caller(bound)?; - let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - Ok(Err(Box::pin(future) as PyValueFuture)) - } else { - let json = py_to_json(bound).map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - Ok(Ok(json)) - } -} - fn split_json_or_future_with_locals( py: Python<'_>, result: Py, @@ -350,6 +436,23 @@ fn copy_middleware_invocation<'py>( Ok((Some(invocation_context), Some(task_locals))) } +fn loop_affine_callback( + py: Python<'_>, + callback: &Bound<'_, PyAny>, + task_locals: Option<&TaskLocals>, + sanitizer: bool, +) -> PyResult> { + if task_locals.is_none() { + return Ok(callback.clone().unbind()); + } + let kwargs = PyDict::new(py); + kwargs.set_item("sanitizer", sanitizer)?; + py.import("nemo_relay._event_sanitizer_context")? + .getattr("loop_affine")? + .call((callback,), Some(&kwargs)) + .map(Bound::unbind) +} + async fn resolve_py_object_or_future( outcome: FlowResult, PyValueFuture>>, ) -> FlowResult> { @@ -361,34 +464,24 @@ async fn resolve_py_object_or_future( fn next_async_iter_coro(async_iter: &Arc>) -> FlowResult>> { Python::attach(|py| { - let iter = async_iter.bind(py); - let next = iter.getattr("__anext__"); - let result = - next.and_then( - |next| match pyo3_async_runtimes::tokio::get_current_locals(py) { - Ok(locals) => locals.context(py).call_method1("run", (next,)), - Err(_) => next.call0(), - }, - ); - match result { - Ok(coro) => Ok(Some(coro.unbind())), - Err(error) => { - if error.is_instance_of::(py) { - Ok(None) - } else { - Err(FlowError::Internal(error.to_string())) - } - } - } + py.import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("async_iter_next")) + .and_then(|next| next.call1((async_iter.bind(py),))) + .map(|coro| Some(coro.unbind())) + .map_err(|error| FlowError::Internal(error.to_string())) }) } -fn schedule_async_iter_task(coro: Py) -> FlowResult> { - Python::attach(|py| { - pyo3_async_runtimes::tokio::get_current_locals(py) - .and_then(|locals| schedule_python_awaitable(py, coro.bind(py), &locals)) - .map_err(|e| FlowError::Internal(e.to_string())) +async fn schedule_async_iter_task(coro: Py) -> FlowResult> { + let receiver = Python::attach(|py| { + let locals = pyo3_async_runtimes::tokio::get_current_locals(py)?; + schedule_python_awaitable(py, coro, &locals).map(|(receiver, _)| receiver) }) + .map_err(|error| FlowError::Internal(error.to_string()))?; + receiver + .await + .map_err(|_| FlowError::Internal("Python awaitable scheduling was cancelled".into()))? + .map_err(|error| FlowError::Internal(error.to_string())) } fn cancel_async_iter_task(task: &Py) -> FlowResult<()> { @@ -452,32 +545,19 @@ async fn await_async_iter_task(task: Py) -> FlowResult> { #[cfg(test)] async fn await_async_iter_value(coro: Py) -> FlowResult> { - await_async_iter_task(schedule_async_iter_task(coro)?).await + await_async_iter_task(schedule_async_iter_task(coro).await?).await } async fn close_async_iter(async_iter: &Arc>) -> FlowResult<()> { let close = Python::attach(|py| { - let iter = async_iter.bind(py); - let close = iter.getattr("aclose"); - let result = - close.and_then( - |close| match pyo3_async_runtimes::tokio::get_current_locals(py) { - Ok(locals) => locals.context(py).call_method1("run", (close,)), - Err(_) => close.call0(), - }, - ); - match result { - Ok(close) => Ok(Some(close.unbind())), - Err(error) if error.is_instance_of::(py) => { - Ok(None) - } - Err(error) => Err(FlowError::Internal(error.to_string())), - } + py.import("nemo_relay._event_sanitizer_context") + .and_then(|module| module.getattr("async_iter_close")) + .and_then(|close| close.call1((async_iter.bind(py),))) + .map(Bound::unbind) + .map_err(|error| FlowError::Internal(error.to_string())) }); - let Some(close) = close? else { - return Ok(()); - }; - let task = schedule_async_iter_task(close)?; + let close = close?; + let task = schedule_async_iter_task(close).await?; let future = Python::attach(|py| { pyo3_async_runtimes::tokio::into_future(task.into_bound(py)) .map_err(|error| FlowError::Internal(error.to_string())) @@ -548,7 +628,7 @@ async fn forward_async_iter( } let next_value = match next_async_iter_coro(&async_iter) { Ok(None) => break Ok(()), - Ok(Some(coro)) => match schedule_async_iter_task(coro) { + Ok(Some(coro)) => match schedule_async_iter_task(coro).await { Ok(task) => { let task_for_future = Python::attach(|py| task.clone_ref(py)); let mut next_value = Box::pin(await_async_iter_task_result(task_for_future)); @@ -676,21 +756,25 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { }; let py_args = json_to_py(py, &args) .map_err(|e| FlowError::Internal(format!("tool json_to_py failed: {e}")))?; - let result = match (invocation_context.as_ref(), publication) { + let loop_affine = task_locals.is_some(); + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), publication) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match (invocation_context.as_ref(), publication && !loop_affine) { (Some(context), true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .and_then(|invoke| { - context.call_method1("run", (invoke, py_fn.bind(py), name, py_args)) + context.call_method1("run", (invoke, callback.bind(py), name, py_args)) }), (Some(context), false) => { - context.call_method1("run", (py_fn.bind(py), name, py_args)) + context.call_method1("run", (callback.bind(py), name, py_args)) } (None, true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) - .and_then(|invoke| invoke.call1((py_fn.bind(py), name, py_args))), - (None, false) => py_fn.bind(py).call1((name, py_args)), + .and_then(|invoke| invoke.call1((callback.bind(py), name, py_args))), + (None, false) => callback.bind(py).call1((name, py_args)), } .map_err(|e| FlowError::Internal(format!("Python tool callback failed: {e}")))?; split_py_object_or_future_with_locals( @@ -722,9 +806,14 @@ pub fn wrap_py_tool_conditional_fn(py_fn: Py) -> ToolConditionalFn { .map_err(|error| FlowError::Internal(error.to_string()))?; let py_args = json_to_py(py, &args).map_err(|e| FlowError::Internal(e.to_string()))?; + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; let result = match invocation_context.as_ref() { - Some(context) => context.call_method1("run", (py_fn.bind(py), name, py_args)), - None => py_fn.bind(py).call1((name, py_args)), + Some(context) => { + context.call_method1("run", (callback.bind(py), name, py_args)) + } + None => callback.bind(py).call1((name, py_args)), } .map_err(|e| FlowError::Internal(e.to_string()))?; split_py_object_or_future_with_locals( @@ -764,9 +853,14 @@ pub fn wrap_py_tool_request_intercept_fn(py_fn: Py) -> ToolInterceptFn { .map_err(|error| FlowError::Internal(error.to_string()))?; let py_args = json_to_py(py, &args).map_err(|e| FlowError::Internal(e.to_string()))?; + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; let result = match invocation_context.as_ref() { - Some(context) => context.call_method1("run", (py_fn.bind(py), name, py_args)), - None => py_fn.bind(py).call1((name, py_args)), + Some(context) => { + context.call_method1("run", (callback.bind(py), name, py_args)) + } + None => callback.bind(py).call1((name, py_args)), } .map_err(|e| FlowError::Internal(e.to_string()))?; split_json_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) @@ -785,45 +879,24 @@ pub fn wrap_py_tool_exec_fn( let py_fn = std::sync::Arc::new(py_fn); Box::new(move |args: Json| { let py_fn = py_fn.clone(); + let task_locals = task_locals_with_running_loop(None); Box::pin(async move { - // Call the Python function and check if it returns a coroutine - let outcome: FlowResult< - Result>> + Send>>>, - > = Python::attach(|py| { + resolve_json_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_args = json_to_py(py, &args).map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - let result = py_fn - .call1(py, (py_args,)) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - - // Detect coroutine by checking for __await__ - let bound = result.bind(py); - if bound.getattr("__await__").is_ok() { - let future = pyo3_async_runtimes::tokio::into_future(result.into_bound(py)) - .map_err(|e| FlowError::Internal(e.to_string()))?; - Ok(Err(Box::pin(future) - as Pin< - Box>> + Send>, - >)) - } else { - let json = - py_to_json(bound).map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - Ok(Ok(json)) - } - }); - - match outcome? { - Ok(json) => Ok(json), - Err(future) => { - let py_result = future - .await - .map_err(|e| FlowError::Internal(e.to_string()))?; - Python::attach(|py| { - py_to_json(py_result.bind(py)) - .map_err(|e: PyErr| FlowError::Internal(e.to_string())) - }) + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match invocation_context.as_ref() { + Some(context) => context.call_method1("run", (callback.bind(py), py_args)), + None => callback.bind(py).call1((py_args,)), } - } + .map_err(|error| FlowError::Internal(error.to_string()))?; + split_json_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) + })) + .await }) }) } @@ -937,11 +1010,14 @@ pub fn wrap_py_tool_exec_intercept_fn( .into_pyobject(py) .map_err(|e| FlowError::Internal(e.to_string()))? .into_any(); + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; let result = match invocation_context.as_ref() { Some(context) => { - context.call_method1("run", (py_fn.bind(py), &name, py_args, py_next)) + context.call_method1("run", (callback.bind(py), &name, py_args, py_next)) } - None => py_fn.bind(py).call1((&name, py_args, py_next)), + None => callback.bind(py).call1((&name, py_args, py_next)), } .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; split_py_object_or_future_with_locals( @@ -1000,11 +1076,14 @@ pub fn wrap_py_llm_exec_intercept_fn( .into_pyobject(py) .map_err(|e| FlowError::Internal(e.to_string()))? .into_any(); + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; let result = match invocation_context.as_ref() { Some(context) => { - context.call_method1("run", (py_fn.bind(py), &name, py_req, py_next)) + context.call_method1("run", (callback.bind(py), &name, py_req, py_next)) } - None => py_fn.bind(py).call1((&name, py_req, py_next)), + None => callback.bind(py).call1((&name, py_req, py_next)), } .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; split_py_object_or_future_with_locals( @@ -1062,11 +1141,14 @@ pub fn wrap_py_llm_stream_exec_intercept_fn( .into_pyobject(py) .map_err(|e: PyErr| FlowError::Internal(e.to_string()))? .into_any(); + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; let result = match invocation_context.as_ref() { Some(context) => { - context.call_method1("run", (py_fn.bind(py), py_req, py_next)) + context.call_method1("run", (callback.bind(py), py_req, py_next)) } - None => py_fn.bind(py).call1((py_req, py_next)), + None => callback.bind(py).call1((py_req, py_next)), } .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; let outcome = split_py_object_or_future_with_locals( @@ -1112,22 +1194,28 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest PyLLMRequest { inner: request }, PyLlmSanitizeRequestContext { inner: context }, ); - let result = match (invocation_context.as_ref(), publication) { + let loop_affine = task_locals.is_some(); + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), publication) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match (invocation_context.as_ref(), publication && !loop_affine) { (Some(context), true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .and_then(|invoke| { - context - .call_method1("run", (invoke, py_fn.bind(py), args.0, args.1)) + context.call_method1( + "run", + (invoke, callback.bind(py), args.0, args.1), + ) }), (Some(context), false) => { - context.call_method1("run", (py_fn.bind(py), args.0, args.1)) + context.call_method1("run", (callback.bind(py), args.0, args.1)) } (None, true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) - .and_then(|invoke| invoke.call1((py_fn.bind(py), args.0, args.1))), - (None, false) => py_fn.bind(py).call1(args), + .and_then(|invoke| invoke.call1((callback.bind(py), args.0, args.1))), + (None, false) => callback.bind(py).call1(args), } .map_err(|e| FlowError::Internal(e.to_string()))?; split_py_object_or_future_with_locals( @@ -1169,9 +1257,12 @@ pub fn wrap_py_llm_conditional_fn(py_fn: Py) -> LlmConditionalFn { let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; let request = PyLLMRequest { inner: request }; + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; let result = match invocation_context.as_ref() { - Some(context) => context.call_method1("run", (py_fn.bind(py), request)), - None => py_fn.bind(py).call1((request,)), + Some(context) => context.call_method1("run", (callback.bind(py), request)), + None => callback.bind(py).call1((request,)), } .map_err(|e| FlowError::Internal(e.to_string()))?; split_py_object_or_future_with_locals( @@ -1232,11 +1323,14 @@ pub fn wrap_py_llm_request_intercept_fn(py_fn: Py) -> LlmRequestIntercept } None => py.None(), }; + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; let result = match invocation_context.as_ref() { Some(context) => { - context.call_method1("run", (py_fn.bind(py), name, py_req, py_ann)) + context.call_method1("run", (callback.bind(py), name, py_req, py_ann)) } - None => py_fn.bind(py).call1((name, py_req, py_ann)), + None => callback.bind(py).call1((name, py_req, py_ann)), } .map_err(|e| { FlowError::Internal(format!("LLM request intercept callable failed: {e}")) @@ -1274,13 +1368,21 @@ pub fn wrap_py_llm_exec_fn( let py_fn = std::sync::Arc::new(py_fn); Box::new(move |request: LlmRequest| { let py_fn = py_fn.clone(); + let task_locals = task_locals_with_running_loop(None); Box::pin(async move { resolve_json_or_future(Python::attach(|py| { + let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_req = PyLLMRequest { inner: request }; - let result = py_fn - .call1(py, (py_req,)) - .map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - split_json_or_future(py, result) + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match invocation_context.as_ref() { + Some(context) => context.call_method1("run", (callback.bind(py), py_req)), + None => callback.bind(py).call1((py_req,)), + } + .map_err(|error| FlowError::Internal(error.to_string()))?; + split_json_or_future_with_locals(py, result.unbind(), task_locals.as_ref()) })) .await }) @@ -1303,14 +1405,30 @@ pub fn wrap_py_llm_stream_exec_fn( let py_fn = std::sync::Arc::new(py_fn); Box::new(move |request: LlmRequest| { let py_fn = py_fn.clone(); + let task_locals = task_locals_with_running_loop(None); Box::pin(async move { - let async_iter: Py = Python::attach(|py| { + let (outcome, invocation_task_locals) = Python::attach(|py| { + let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) + .map_err(|error| FlowError::Internal(error.to_string()))?; let py_req = PyLLMRequest { inner: request }; - py_fn - .call1(py, (py_req,)) - .map_err(|e: PyErr| FlowError::Internal(e.to_string())) + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), false) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match invocation_context.as_ref() { + Some(context) => context.call_method1("run", (callback.bind(py), py_req)), + None => callback.bind(py).call1((py_req,)), + } + .map_err(|error| FlowError::Internal(error.to_string()))?; + let outcome = split_py_object_or_future_with_locals( + py, + result.unbind(), + task_locals.as_ref(), + invocation_context.as_ref(), + )?; + Ok::<_, FlowError>((outcome, task_locals)) })?; - stream_from_async_iter(async_iter, None) + let async_iter = resolve_py_object_or_future(Ok(outcome)).await?; + stream_from_async_iter(async_iter, invocation_task_locals) }) }) } @@ -1385,24 +1503,30 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon let py_context = PyLlmSanitizeResponseContext { inner: context }; let py_response = json_to_py(py, &response) .map_err(|error| FlowError::Internal(error.to_string()))?; - let result = match (invocation_context.as_ref(), publication) { + let loop_affine = task_locals.is_some(); + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), publication) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match (invocation_context.as_ref(), publication && !loop_affine) { (Some(context), true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .and_then(|invoke| { context.call_method1( "run", - (invoke, py_fn.bind(py), py_response, py_context), + (invoke, callback.bind(py), py_response, py_context), ) }), (Some(context), false) => { - context.call_method1("run", (py_fn.bind(py), py_response, py_context)) + context.call_method1("run", (callback.bind(py), py_response, py_context)) } (None, true) => py .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) - .and_then(|invoke| invoke.call1((py_fn.bind(py), py_response, py_context))), - (None, false) => py_fn.bind(py).call1((py_response, py_context)), + .and_then(|invoke| { + invoke.call1((callback.bind(py), py_response, py_context)) + }), + (None, false) => callback.bind(py).call1((py_response, py_context)), } .map_err(|error| FlowError::Internal(error.to_string()))?; split_py_object_or_future_with_locals( @@ -1522,10 +1646,18 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { .import("nemo_relay._event_sanitizer_context") .and_then(|module| module.getattr("invoke")) .map_err(|error| FlowError::Internal(error.to_string()))?; - let result = match invocation_context.as_ref() { - Some(context) => context - .call_method1("run", (invoke, py_fn.bind(py), py_event, py_fields)), - None => invoke.call1((py_fn.bind(py), py_event, py_fields)), + let loop_affine = task_locals.is_some(); + let callback = + loop_affine_callback(py, py_fn.bind(py), task_locals.as_ref(), true) + .map_err(|error| FlowError::Internal(error.to_string()))?; + let result = match (invocation_context.as_ref(), !loop_affine) { + (Some(context), true) => context + .call_method1("run", (invoke, callback.bind(py), py_event, py_fields)), + (None, true) => invoke.call1((callback.bind(py), py_event, py_fields)), + (Some(context), false) => { + context.call_method1("run", (callback.bind(py), py_event, py_fields)) + } + (None, false) => callback.bind(py).call1((py_event, py_fields)), } .map_err(|error| FlowError::Internal(error.to_string()))?; split_py_object_or_future_with_locals( diff --git a/crates/python/src/test_support.rs b/crates/python/src/test_support.rs index e4986103a..3cb62dab8 100644 --- a/crates/python/src/test_support.rs +++ b/crates/python/src/test_support.rs @@ -1,10 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use std::ffi::OsString; +use std::ffi::{CString, OsString}; use std::sync::{Mutex, MutexGuard, OnceLock}; -use pyo3::Python; +use pyo3::prelude::*; +use pyo3::types::PyModule; const BINDING_KIND_ENV: &str = "NEMO_RELAY_BINDING_KIND"; const RUNTIME_OWNER_ENV: &str = "NEMO_RELAY_RUNTIME_OWNER"; @@ -65,6 +66,38 @@ pub(crate) fn init_python_test_locked(lock: MutexGuard<'static, ()>) -> PythonTe std::env::set_var(XDG_CONFIG_HOME_ENV, isolated_config_home); } Python::initialize(); + Python::attach(|py| { + let sys_modules = py + .import("sys") + .expect("import sys") + .getattr("modules") + .expect("sys.modules"); + if sys_modules + .contains("nemo_relay._event_sanitizer_context") + .expect("inspect test modules") + { + return; + } + let package = PyModule::new(py, "nemo_relay").expect("create test package"); + package + .setattr("__path__", Vec::::new()) + .expect("mark test package"); + sys_modules + .set_item("nemo_relay", package) + .expect("register test package"); + let source = CString::new(include_str!( + "../../../python/nemo_relay/_event_sanitizer_context.py" + )) + .expect("helper source"); + let filename = CString::new("_event_sanitizer_context.py").expect("helper filename"); + let module_name = + CString::new("nemo_relay._event_sanitizer_context").expect("helper module name"); + let helper = PyModule::from_code(py, &source, &filename, &module_name) + .expect("load async callback helpers"); + sys_modules + .set_item("nemo_relay._event_sanitizer_context", helper) + .expect("register async callback helpers"); + }); PythonTestGuard { _lock: lock, binding_kind, diff --git a/crates/python/tests/coverage/nemo_guardrails_coverage_tests.rs b/crates/python/tests/coverage/nemo_guardrails_coverage_tests.rs index eeadf19be..ce3a4f274 100644 --- a/crates/python/tests/coverage/nemo_guardrails_coverage_tests.rs +++ b/crates/python/tests/coverage/nemo_guardrails_coverage_tests.rs @@ -315,6 +315,23 @@ fn with_event_loop(py: Python<'_>, f: impl FnOnce(Bound<'_, PyAny>) -> T) -> .call_method1("set_event_loop", (&event_loop,)) .unwrap(); let result = catch_unwind(AssertUnwindSafe(|| f(event_loop.clone().into_any()))); + let drain = PyModule::from_code( + py, + &CString::new( + "import asyncio\nasync def drain(loop):\n current = asyncio.current_task(loop)\n pending = asyncio.all_tasks(loop) - {current}\n for task in pending:\n task.cancel()\n if pending:\n await asyncio.gather(*pending, return_exceptions=True)\n", + ) + .unwrap(), + &CString::new("drain_test_loop.py").unwrap(), + &CString::new("drain_test_loop").unwrap(), + ) + .unwrap() + .getattr("drain") + .unwrap() + .call1((&event_loop,)) + .unwrap(); + event_loop + .call_method1("run_until_complete", (drain,)) + .unwrap(); asyncio .call_method1("set_event_loop", (py.None(),)) .unwrap(); diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index 3f328fca7..df5b86afc 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -419,18 +419,6 @@ async def coro_non_json(): let coro_cancel_fn: Py = module.getattr("coro_cancel").unwrap().unbind(); let coro_non_json_fn: Py = module.getattr("coro_non_json").unwrap().unbind(); - assert!( - next_async_iter_coro(&Arc::new(stop_iter_cls.call0(py).unwrap())) - .unwrap() - .is_none() - ); - assert!( - next_async_iter_coro(&Arc::new(error_iter_cls.call0(py).unwrap())) - .unwrap_err() - .to_string() - .contains("next boom") - ); - let value_payload = crate::convert::json_to_py(py, &json!({"x": 1})).unwrap(); let dropped_payload = crate::convert::json_to_py(py, &json!({"x": 2})).unwrap(); let no_loop_payload = crate::convert::json_to_py(py, &json!({"x": 3})).unwrap(); @@ -439,6 +427,26 @@ async def coro_non_json(): with_event_loop(py, |event_loop| { let _runtime = tokio::runtime::Runtime::new().unwrap(); pyo3_async_runtimes::tokio::run_until_complete(event_loop, async move { + let stop = next_async_iter_coro(&Arc::new(Python::attach(|py| { + stop_iter_cls.call0(py).unwrap() + }))) + .unwrap() + .unwrap(); + assert!(await_async_iter_value(stop).await.unwrap().is_none()); + + let error = next_async_iter_coro(&Arc::new(Python::attach(|py| { + error_iter_cls.call0(py).unwrap() + }))) + .unwrap() + .unwrap(); + assert!( + await_async_iter_value(error) + .await + .unwrap_err() + .to_string() + .contains("next boom") + ); + let value = await_async_iter_value(Python::attach(|py| coro_value_fn.call0(py).unwrap())) .await diff --git a/python/nemo_relay/_event_sanitizer_context.py b/python/nemo_relay/_event_sanitizer_context.py index 868cb35cf..88ab27db2 100644 --- a/python/nemo_relay/_event_sanitizer_context.py +++ b/python/nemo_relay/_event_sanitizer_context.py @@ -44,6 +44,30 @@ async def await_result(result: Awaitable[Any]) -> Any: return await result +def loop_affine(callback: Callable[..., Any], *, sanitizer: bool = False) -> Callable[..., Awaitable[Any]]: + """Defer a callback's synchronous prelude to the awaiting event-loop task.""" + + async def wrapped(*args: Any) -> Any: + result = invoke(callback, *args) if sanitizer else callback(*args) + if inspect.isawaitable(result): + return await result + return result + + return wrapped + + +async def async_iter_next(iterator: Any) -> Any: + """Invoke and await ``__anext__`` on the current event-loop thread.""" + return await iterator.__anext__() + + +async def async_iter_close(iterator: Any) -> None: + """Invoke and await ``aclose`` on the current event-loop thread when present.""" + close = getattr(iterator, "aclose", None) + if close is not None: + await close() + + def invoke(callback: Callable[..., Any], *args: Any) -> Any: """Invoke a sanitizer while marking its sync and async execution contexts.""" state = _ACTIVE.get() diff --git a/python/nemo_relay/tools.py b/python/nemo_relay/tools.py index 51995c059..1f53127c2 100644 --- a/python/nemo_relay/tools.py +++ b/python/nemo_relay/tools.py @@ -188,7 +188,13 @@ async def local_tool(args): """ ensure_scope_stack() return _native_tool_call_execute( - name, args, func, handle=handle, attributes=attributes, data=data, metadata=metadata + name, + args, + func, + handle=handle, + attributes=attributes, + data=data, + metadata=metadata, ) diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 548b9e2ea..a3dcf3fd8 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -13,6 +13,7 @@ import nemo_relay from nemo_relay import EventSanitizeFields, guardrails, plugin, scope, scope_local, subscribers +from nemo_relay._event_sanitizer_context import callback_active, loop_affine @pytest.fixture(name="capture_events") @@ -183,6 +184,34 @@ async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> Even guardrails.deregister_mark_sanitize("python-descendant-flush-liveness") +async def test_cancelled_sanitizer_expires_descendant_context(): + descendant_started = asyncio.Event() + release_descendant = asyncio.Event() + observed: list[bool] = [] + + async def descendant() -> None: + descendant_started.set() + await release_descendant.wait() + observed.append(callback_active()) + + async def never() -> None: + await asyncio.Event().wait() + + def sanitizer() -> object: + asyncio.create_task(descendant()) + return never() + + execution = asyncio.ensure_future(loop_affine(sanitizer, sanitizer=True)()) + await asyncio.wait_for(descendant_started.wait(), timeout=1) + execution.cancel() + with pytest.raises(asyncio.CancelledError): + await execution + release_descendant.set() + await asyncio.sleep(0) + + assert observed == [False] + + def test_sync_mark_sanitizer_uses_emitter_context(capture_events): request_id = contextvars.ContextVar("request_id", default="registration") observed: list[str] = [] diff --git a/python/tests/test_llm.py b/python/tests/test_llm.py index fbde382e3..6263ed838 100644 --- a/python/tests/test_llm.py +++ b/python/tests/test_llm.py @@ -12,6 +12,7 @@ import pytest from nemo_relay import ( + Event, LLMAttributes, LLMHandle, LLMRequest, @@ -609,6 +610,7 @@ async def test_cancelling_execute_cancels_pending_execution_intercept(self): release = asyncio.Event() cancelled = asyncio.Event() provider_calls: list[LLMRequest] = [] + events: list[Event] = [] async def middleware(_name, request, next): started.set() @@ -624,6 +626,7 @@ def provider(request): return {"ok": True} intercepts.register_llm_execution("py_llm_cancel_intercept", 1, middleware) + subscribers.register("py_llm_cancel_events", events.append) try: execution = asyncio.ensure_future(llm.execute("cancel_llm", make_request(), provider)) await asyncio.wait_for(started.wait(), timeout=1) @@ -631,19 +634,24 @@ def provider(request): with pytest.raises(asyncio.CancelledError): await execution await asyncio.wait_for(cancelled.wait(), timeout=1) - release.set() - await asyncio.sleep(0) + await subscribers.flush_async() finally: release.set() intercepts.deregister_llm_execution("py_llm_cancel_intercept") + subscribers.deregister("py_llm_cancel_events") assert provider_calls == [] + lifecycle = [ + event.scope_category for event in events if isinstance(event, ScopeEvent) and event.name == "cancel_llm" + ] + assert lifecycle == ["start", "end"] async def test_cancelling_stream_execute_cancels_pending_stream_intercept(self): started = asyncio.Event() release = asyncio.Event() cancelled = asyncio.Event() provider_calls: list[LLMRequest] = [] + events: list[Event] = [] async def middleware(request, next): started.set() @@ -663,6 +671,7 @@ async def generate(): return generate() intercepts.register_llm_stream_execution("py_llm_stream_cancel_intercept", 1, middleware) + subscribers.register("py_llm_stream_cancel_events", events.append) try: execution = asyncio.ensure_future( llm.stream_execute( @@ -678,13 +687,19 @@ async def generate(): with pytest.raises(asyncio.CancelledError): await execution await asyncio.wait_for(cancelled.wait(), timeout=1) - release.set() - await asyncio.sleep(0) + await subscribers.flush_async() finally: release.set() intercepts.deregister_llm_stream_execution("py_llm_stream_cancel_intercept") + subscribers.deregister("py_llm_stream_cancel_events") assert provider_calls == [] + lifecycle = [ + event.scope_category + for event in events + if isinstance(event, ScopeEvent) and event.name == "cancel_stream_llm" + ] + assert lifecycle == ["start", "end"] async def test_sync_middleware_preserves_async_caller_context(self): request_id = contextvars.ContextVar("llm_middleware_request_id", default="registration") diff --git a/python/tests/test_tools.py b/python/tests/test_tools.py index b1f034d43..ec2f6b055 100644 --- a/python/tests/test_tools.py +++ b/python/tests/test_tools.py @@ -5,6 +5,8 @@ import asyncio import contextvars +import gc +import warnings from collections import UserDict, UserList from dataclasses import dataclass from typing import cast @@ -441,6 +443,64 @@ def test_request_intercept_raises_on_unserializable_return(self): class TestToolInterceptsAsync: + def test_loop_shutdown_cancels_pending_middleware_without_unraisable_errors(self, capsys): + async def request_intercept(_name, args): + await asyncio.Event().wait() + return args + + async def scenario(): + execution = asyncio.ensure_future(tools.execute("shutdown_tool", {}, lambda args: args)) + await asyncio.sleep(0.01) + assert not execution.done() + + intercepts.register_tool_request("py_tool_shutdown_request", 1, False, request_intercept) + try: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + asyncio.run(scenario()) + gc.collect() + finally: + intercepts.deregister_tool_request("py_tool_shutdown_request") + + diagnostics = capsys.readouterr().err + "\n".join(str(item.message) for item in caught) + assert "Event loop is closed" not in diagnostics + assert "Task was destroyed but it is pending" not in diagnostics + assert "was never awaited" not in diagnostics + + async def test_cancelling_conditional_guardrail_closes_guardrail_scope(self): + started = asyncio.Event() + cancelled = asyncio.Event() + events: list[Event] = [] + + async def conditional(_name, _args): + started.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + cancelled.set() + raise + + guardrails.register_tool_conditional_execution("py_tool_cancel_conditional", 1, conditional) + subscribers.register("py_tool_cancel_conditional_events", events.append) + try: + execution = asyncio.ensure_future(tools.execute("cancel_conditional_tool", {}, lambda x: x)) + await asyncio.wait_for(started.wait(), timeout=1) + execution.cancel() + with pytest.raises(asyncio.CancelledError): + await execution + await asyncio.wait_for(cancelled.wait(), timeout=1) + await subscribers.flush_async() + finally: + guardrails.deregister_tool_conditional_execution("py_tool_cancel_conditional") + subscribers.deregister("py_tool_cancel_conditional_events") + + guardrail_lifecycle = [ + event.scope_category + for event in events + if isinstance(event, ScopeEvent) and event.name == "py_tool_cancel_conditional" + ] + assert guardrail_lifecycle == ["start", "end"] + async def test_cancelling_execute_cancels_pending_request_intercept(self): started = asyncio.Event() cancelled = asyncio.Event() @@ -477,6 +537,7 @@ async def test_cancelling_execute_cancels_pending_execution_intercept(self): release = asyncio.Event() cancelled = asyncio.Event() provider_calls: list[dict] = [] + events: list[Event] = [] async def middleware(_name, args, next): started.set() @@ -492,6 +553,7 @@ def provider(args): return args intercepts.register_tool_execution("py_tool_cancel_intercept", 1, middleware) + subscribers.register("py_tool_cancel_events", events.append) try: execution = asyncio.ensure_future(tools.execute("cancel_tool", {"ok": True}, provider)) await asyncio.wait_for(started.wait(), timeout=1) @@ -499,13 +561,17 @@ def provider(args): with pytest.raises(asyncio.CancelledError): await execution await asyncio.wait_for(cancelled.wait(), timeout=1) - release.set() - await asyncio.sleep(0) + await subscribers.flush_async() finally: release.set() intercepts.deregister_tool_execution("py_tool_cancel_intercept") + subscribers.deregister("py_tool_cancel_events") assert provider_calls == [] + lifecycle = [ + event.scope_category for event in events if isinstance(event, ScopeEvent) and event.name == "cancel_tool" + ] + assert lifecycle == ["start", "end"] async def test_sync_middleware_preserves_async_caller_context(self): request_id = contextvars.ContextVar("tool_middleware_request_id", default="registration") From affbe3dbb5cb23d926c30dbeea578391542aff16 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 06:42:17 -0400 Subject: [PATCH 51/83] fix: preserve async middleware execution context Signed-off-by: Will Killian --- crates/core/src/api/runtime.rs | 4 +- crates/core/src/api/runtime/scope_stack.rs | 16 +++ .../src/api/runtime/subscriber_dispatcher.rs | 30 +++- crates/core/src/context/registries.rs | 1 + crates/core/src/plugin/dynamic/native.rs | 64 +++++---- crates/core/src/plugin/dynamic/worker.rs | 104 ++++++++++++-- crates/core/src/registry.rs | 1 + .../tests/fixtures/worker_plugin/src/main.rs | 36 ++--- .../subscriber_dispatcher_tests.rs | 59 +++++++- .../core/tests/unit/dynamic_worker_tests.rs | 28 ++-- crates/core/tests/unit/native_plugin_tests.rs | 128 ++++++++++++++++++ .../tests/unit/observability/atof_tests.rs | 2 +- crates/node/src/callback_factory.rs | 7 +- crates/node/tests/event_sanitizers_tests.mjs | 19 ++- crates/node/tests/llm_tests.mjs | 27 ++++ crates/node/tests/tools_tests.mjs | 14 ++ crates/python/src/py_callable.rs | 22 ++- crates/worker/README.md | 2 +- crates/worker/src/lib.rs | 101 +++++++------- crates/worker/tests/worker_sdk_tests.rs | 59 +++++--- python/tests/test_dynamic_plugin_host.py | 49 ++++++- python/tests/test_event_sanitizers.py | 33 +++++ 22 files changed, 653 insertions(+), 153 deletions(-) diff --git a/crates/core/src/api/runtime.rs b/crates/core/src/api/runtime.rs index 77670c804..f27ad4fb0 100644 --- a/crates/core/src/api/runtime.rs +++ b/crates/core/src/api/runtime.rs @@ -23,8 +23,8 @@ pub use scope_stack::{ capture_propagation_context, capture_propagation_context_with_root, capture_thread_scope_stack, create_scope_stack, create_scope_stack_from_propagation, current_scope_stack, propagate_scope_to_thread, restore_thread_scope_stack, scope_stack_active, - set_thread_scope_stack, sync_thread_scope_stack, task_scope_push, task_scope_remove, - task_scope_top, with_active_event_uuid, with_scope_stack, + set_thread_scope_stack, snapshot_scope_stack, sync_thread_scope_stack, task_scope_push, + task_scope_remove, task_scope_top, with_active_event_uuid, with_scope_stack, }; pub use state::NemoRelayContextState; pub use subscriber_dispatcher::flush_subscribers; diff --git a/crates/core/src/api/runtime/scope_stack.rs b/crates/core/src/api/runtime/scope_stack.rs index fe0cfa6c9..218df680d 100644 --- a/crates/core/src/api/runtime/scope_stack.rs +++ b/crates/core/src/api/runtime/scope_stack.rs @@ -29,6 +29,7 @@ use crate::registry::{RegistryEntry, SortedRegistry}; /// their nearest agent's freshness instead of creating a separate budget. /// Additional scopes are pushed as the public API opens lifecycle spans and /// removed when those spans close. +#[derive(Clone)] pub struct ScopeStack { stack: Vec, scope_registries: HashMap, @@ -406,6 +407,16 @@ pub fn create_scope_stack() -> ScopeStackHandle { Arc::new(RwLock::new(ScopeStack::new())) } +/// Clone a scope stack into an isolated emission-time snapshot. +#[doc(hidden)] +pub fn snapshot_scope_stack(handle: &ScopeStackHandle) -> Result { + let stack = handle + .read() + .unwrap_or_else(|error| error.into_inner()) + .clone(); + Ok(Arc::new(RwLock::new(stack))) +} + /// Create an isolated scope stack rooted below a supplied propagation context. /// /// The imported handles are synthetic bookkeeping only; Relay never emits their @@ -450,6 +461,11 @@ pub async fn with_active_event_uuid(uuid: Uuid, future: impl Future Option { + ACTIVE_EVENT_UUID.try_with(|uuid| *uuid).ok() +} + thread_local! { /// Synchronous override used by native plugin callbacks that need to run a /// bounded block with an isolated stack even inside a task-local context. diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index f68e95956..42e9e39dd 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -94,7 +94,7 @@ mod native { use crate::api::runtime::scope_stack::current_scope_stack; use crate::api::runtime::scope_stack::{ ScopeStackHandle, capture_thread_scope_stack, restore_thread_scope_stack, - set_thread_scope_stack, + set_thread_scope_stack, snapshot_scope_stack, }; use crate::error::FlowError; @@ -195,6 +195,20 @@ mod native { } } + fn immutable_scope_stack(scope_stack: &ScopeStackHandle) -> Option { + match snapshot_scope_stack(scope_stack) { + Ok(scope_stack) => Some(scope_stack), + Err(error) => { + log::error!( + target: "nemo_relay.runtime", + event = "subscriber_scope_snapshot_failed"; + "Queued publication could not snapshot its emitting scope stack: {error}" + ); + None + } + } + } + #[cfg(test)] pub(super) fn block_on_sanitizer_future( future: F, @@ -282,12 +296,15 @@ mod native { if subscribers.is_empty() { return true; } + let Some(scope_stack) = immutable_scope_stack(¤t_scope_stack()) else { + return false; + }; let message = DispatcherMessage::Deliver { event: Box::new(event.clone()), transform: None, sanitizers: Vec::new(), subscribers: subscribers.to_vec(), - scope_stack: current_scope_stack(), + scope_stack, publication_context: current_publication_context(), }; send_dispatch_message(message) @@ -302,6 +319,9 @@ mod native { if subscribers.is_empty() { return true; } + let Some(scope_stack) = immutable_scope_stack(&scope_stack) else { + return false; + }; let message = DispatcherMessage::Deliver { event: Box::new(event), transform: None, @@ -322,6 +342,9 @@ mod native { if subscribers.is_empty() { return true; } + let Some(scope_stack) = immutable_scope_stack(&scope_stack) else { + return false; + }; let message = DispatcherMessage::Deliver { event: Box::new(event), transform: None, @@ -354,6 +377,9 @@ mod native { subscribers: &[EventSubscriberFn], scope_stack: ScopeStackHandle, ) -> bool { + let Some(scope_stack) = immutable_scope_stack(&scope_stack) else { + return false; + }; let message = DispatcherMessage::Deliver { event: Box::new(event), transform: Some(transform), diff --git a/crates/core/src/context/registries.rs b/crates/core/src/context/registries.rs index bf59827a9..098d818c8 100644 --- a/crates/core/src/context/registries.rs +++ b/crates/core/src/context/registries.rs @@ -23,6 +23,7 @@ use crate::registry::SortedRegistry; /// subscribers. These registrations are merged with the global runtime /// registries when the runtime resolves the effective middleware chain for a /// tool or LLM call executed inside that scope. +#[derive(Clone)] pub(crate) struct ScopeLocalRegistries { /// Mark event field sanitizers. pub(crate) mark_sanitize_guardrails: SortedRegistry>, diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 68793b581..0eae9c148 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -1432,6 +1432,7 @@ struct NativeAsyncNext { struct NativeAsyncStream { sender: Mutex>>>, cancelled: AtomicBool, + next_invoked: AtomicBool, downstream_abort: Mutex>, _callback_user_data: Option>, } @@ -1813,41 +1814,47 @@ unsafe extern "C" fn native_async_next_invoke( ); return NemoRelayStatus::InvalidArg; } + enum Invocation { + Tool(Json), + Llm(LlmRequest), + } + let invocation = match &next.inner { + NativeAsyncNextInner::Tool(_) => Invocation::Tool(invocation), + NativeAsyncNextInner::Llm(_) => match serde_json::from_value(invocation) { + Ok(request) => Invocation::Llm(request), + Err(error) => { + set_native_last_error(error.to_string()); + return NemoRelayStatus::InvalidJson; + } + }, + NativeAsyncNextInner::LlmStream(_) => unreachable!("stream continuations were rejected"), + }; unsafe { Arc::increment_strong_count(completion as *const NativeAsyncCompletion) }; let completion = unsafe { Arc::from_raw(completion as *const NativeAsyncCompletion) }; if completion.cancelled.load(Ordering::Acquire) { return NemoRelayStatus::InvalidArg; } - let future: Pin> + Send>> = match &next.inner { - NativeAsyncNextInner::Tool(next) => { - let next = next.clone(); - Box::pin(async move { - serde_json::to_value(ToolExecutionInterceptOutcome::new(next(invocation).await?)) + let future: Pin> + Send>> = + match (&next.inner, invocation) { + (NativeAsyncNextInner::Tool(next), Invocation::Tool(invocation)) => { + let next = next.clone(); + Box::pin(async move { + serde_json::to_value(ToolExecutionInterceptOutcome::new( + next(invocation).await?, + )) .map_err(|error| { FlowError::Internal(format!( "failed to serialize native async tool outcome: {error}" )) }) - }) - } - NativeAsyncNextInner::Llm(next) => { - let request = match serde_json::from_value(invocation) { - Ok(request) => request, - Err(error) => { - let _ = completion - .sender - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - .map(|sender| sender.send(Err(FlowError::Internal(error.to_string())))); - return NemoRelayStatus::InvalidArg; - } - }; - let next = next.clone(); - Box::pin(async move { next(request).await }) - } - NativeAsyncNextInner::LlmStream(_) => unreachable!("stream continuations were rejected"), - }; + }) + } + (NativeAsyncNextInner::Llm(next), Invocation::Llm(request)) => { + let next = next.clone(); + Box::pin(async move { next(request).await }) + } + _ => unreachable!("native next invocation kind matched its continuation"), + }; let scope_stack = next.scope_stack.clone(); let mut abort_guard = completion .next_abort @@ -1913,6 +1920,12 @@ unsafe extern "C" fn native_async_next_invoke_stream( Ok(request) => request, Err(status) => return status, }; + if output_stream.next_invoked.swap(true, Ordering::AcqRel) { + set_native_last_error( + "native async stream next was already invoked for this output stream", + ); + return NemoRelayStatus::InvalidArg; + } let next_fn = next_fn.clone(); let scope_stack = next.scope_stack.clone(); let library_guard = next._callback_user_data.clone(); @@ -2282,6 +2295,7 @@ fn wrap_native_incremental_llm_stream_execution( let stream = Arc::new(NativeAsyncStream { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), downstream_abort: Mutex::new(None), _callback_user_data: Some(user_data.clone()), }); diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 642bd1df1..3a63e94f5 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -58,10 +58,15 @@ use tower::service_fn; use crate::api::event::{Event, EventSanitizeFields}; use crate::api::llm::{LLM_REQUEST_INTERCEPT_OUTCOME_SCHEMA, LlmRequest}; +use crate::api::runtime::scope_stack::active_event_uuid; +use crate::api::runtime::subscriber_dispatcher::{ + PublicationContext, capture_publication_context, with_task_publication_context, +}; use crate::api::runtime::{ EventSanitizeFn, LlmCodecIdentity, LlmExecutionNextFn, LlmJsonStream, LlmSanitizeRequestContext, LlmSanitizeResponseContext, LlmStreamExecutionNextFn, - ToolExecutionNextFn, current_scope_stack, with_scope_stack, + TASK_SCOPE_STACK, ToolExecutionNextFn, current_scope_stack, with_active_event_uuid, + with_scope_stack, }; use crate::api::scope::{ EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeHandle, ScopeType, @@ -1553,7 +1558,7 @@ impl WorkerPluginCallback { ) -> FlowResult { let continuation_id = self .host_state - .insert_continuation(Continuation::Tool(next))?; + .insert_continuation(Continuation::tool(next))?; let request = self.base_request( registration_name, RegistrationSurface::ToolExecutionIntercept, @@ -1743,7 +1748,7 @@ impl WorkerPluginCallback { ) -> FlowResult { let continuation_id = self .host_state - .insert_continuation(Continuation::Llm(next))?; + .insert_continuation(Continuation::llm(next))?; let invoke = self.base_request( registration_name, RegistrationSurface::LlmExecutionIntercept, @@ -1767,7 +1772,7 @@ impl WorkerPluginCallback { ) -> FlowResult { let continuation_id = self .host_state - .insert_continuation(Continuation::LlmStream(next))?; + .insert_continuation(Continuation::llm_stream(next))?; let invoke = self.base_request( registration_name, RegistrationSurface::LlmStreamExecutionIntercept, @@ -2392,11 +2397,69 @@ impl WorkerHostRuntimeState { } } +#[derive(Clone)] +struct ContinuationContext { + scope_stack: crate::api::runtime::ScopeStackHandle, + active_event_uuid: Option, + publication_context: Option, +} + +impl ContinuationContext { + fn capture() -> Self { + Self { + scope_stack: current_scope_stack(), + active_event_uuid: active_event_uuid(), + publication_context: capture_publication_context(), + } + } + + async fn run(&self, future: F) -> F::Output { + let scoped = TASK_SCOPE_STACK.scope(self.scope_stack.clone(), future); + let published = with_task_publication_context(self.publication_context.clone(), scoped); + match self.active_event_uuid { + Some(uuid) => with_active_event_uuid(uuid, published).await, + None => published.await, + } + } +} + #[derive(Clone)] enum Continuation { - Tool(ToolExecutionNextFn), - Llm(LlmExecutionNextFn), - LlmStream(LlmStreamExecutionNextFn), + Tool { + next: ToolExecutionNextFn, + context: ContinuationContext, + }, + Llm { + next: LlmExecutionNextFn, + context: ContinuationContext, + }, + LlmStream { + next: LlmStreamExecutionNextFn, + context: ContinuationContext, + }, +} + +impl Continuation { + fn tool(next: ToolExecutionNextFn) -> Self { + Self::Tool { + next, + context: ContinuationContext::capture(), + } + } + + fn llm(next: LlmExecutionNextFn) -> Self { + Self::Llm { + next, + context: ContinuationContext::capture(), + } + } + + fn llm_stream(next: LlmStreamExecutionNextFn) -> Self { + Self::LlmStream { + next, + context: ContinuationContext::capture(), + } + } } struct WorkerHostRuntimeService { @@ -2560,7 +2623,7 @@ impl RelayHostRuntime for WorkerHostRuntimeService { self.state .authorize(&request.activation_id, &request.auth_token)?; let continuation = self.state.continuation(&request.continuation_id)?; - let Continuation::Tool(next) = continuation else { + let Continuation::Tool { next, context } = continuation else { return Err(Status::invalid_argument( "continuation is not a tool continuation", )); @@ -2569,7 +2632,7 @@ impl RelayHostRuntime for WorkerHostRuntimeService { required_envelope(request.value, "tool next value").map_err(status_from_flow)?; let value = decode_json_envelope::(&value) .map_err(|err| Status::invalid_argument(format!("invalid tool next JSON: {err}")))?; - let result = next(value).await; + let result = context.run(next(value)).await; Ok(Response::new(json_result(result))) } @@ -2581,7 +2644,7 @@ impl RelayHostRuntime for WorkerHostRuntimeService { self.state .authorize(&request.activation_id, &request.auth_token)?; let continuation = self.state.continuation(&request.continuation_id)?; - let Continuation::Llm(next) = continuation else { + let Continuation::Llm { next, context } = continuation else { return Err(Status::invalid_argument( "continuation is not an LLM continuation", )); @@ -2590,7 +2653,7 @@ impl RelayHostRuntime for WorkerHostRuntimeService { required_envelope(request.request, "llm next request").map_err(status_from_flow)?; let request = decode_json_envelope::(&request) .map_err(|err| Status::invalid_argument(format!("invalid LLM next request: {err}")))?; - let result = next(request).await; + let result = context.run(next(request)).await; Ok(Response::new(json_result(result))) } @@ -2605,7 +2668,7 @@ impl RelayHostRuntime for WorkerHostRuntimeService { self.state .authorize(&request.activation_id, &request.auth_token)?; let continuation = self.state.continuation(&request.continuation_id)?; - let Continuation::LlmStream(next) = continuation else { + let Continuation::LlmStream { next, context } = continuation else { return Err(Status::invalid_argument( "continuation is not an LLM stream continuation", )); @@ -2615,8 +2678,21 @@ impl RelayHostRuntime for WorkerHostRuntimeService { let request = decode_json_envelope::(&request).map_err(|err| { Status::invalid_argument(format!("invalid LLM stream next request: {err}")) })?; - let stream = next(request).await.map_err(status_from_flow)?; - let mapped = stream.map(|item| match item { + let stream = context.run(next(request)).await.map_err(status_from_flow)?; + let (tx, rx) = mpsc::channel(16); + tokio::spawn(async move { + context + .run(async move { + let mut stream = stream; + while let Some(item) = stream.next().await { + if tx.send(item).await.is_err() { + break; + } + } + }) + .await; + }); + let mapped = tokio_stream::wrappers::ReceiverStream::new(rx).map(|item| match item { Ok(value) => Ok(StreamChunk { item: Some(stream_chunk_item::Item::Value(json_envelope_infallible( JSON_SCHEMA, diff --git a/crates/core/src/registry.rs b/crates/core/src/registry.rs index 2d85c78f0..d531e421a 100644 --- a/crates/core/src/registry.rs +++ b/crates/core/src/registry.rs @@ -40,6 +40,7 @@ pub(crate) trait RegistryEntry { /// Names must be unique within a registry. Attempting to [`register`](SortedRegistry::register) /// a duplicate name returns an error. Use [`deregister`](SortedRegistry::deregister) first /// to remove an existing entry before re-registering. +#[derive(Clone)] pub(crate) struct SortedRegistry { entries: HashMap, sorted_keys: Vec, diff --git a/crates/core/tests/fixtures/worker_plugin/src/main.rs b/crates/core/tests/fixtures/worker_plugin/src/main.rs index a9dd41a67..10917e56b 100644 --- a/crates/core/tests/fixtures/worker_plugin/src/main.rs +++ b/crates/core/tests/fixtures/worker_plugin/src/main.rs @@ -151,7 +151,7 @@ fn register_fixture_tool_hooks( ctx.register_tool_conditional_execution_guardrail( "fixture_tool_conditional", 0, - move |_name, _args| { + move |_name, _args| async move { if block_tool { Ok(Some("fixture tool blocked".into())) } else { @@ -160,19 +160,19 @@ fn register_fixture_tool_hooks( }, ); ctx.register_tool_request_intercept("fixture_rewrite_args", 0, false, move |_name, args| { - if exit_in_tool_request { - std::process::exit(44); - } - if tool_request_error { - return Err(WorkerSdkError::Callback( - "fixture tool request error requested".into(), - )); - } let runtime = runtime.clone(); - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(emit_runtime_events(runtime)) - })?; - Ok(mark_json(args, "worker_plugin")) + async move { + if exit_in_tool_request { + std::process::exit(44); + } + if tool_request_error { + return Err(WorkerSdkError::Callback( + "fixture tool request error requested".into(), + )); + } + emit_runtime_events(runtime).await?; + Ok(mark_json(args, "worker_plugin")) + } }); ctx.register_tool_execution_intercept( "fixture_tool_execution", @@ -219,14 +219,16 @@ fn register_fixture_llm_hooks( ))) }, ); - ctx.register_llm_conditional_execution_guardrail("fixture_llm_conditional", 0, |_request| { - Ok(None) - }); + ctx.register_llm_conditional_execution_guardrail( + "fixture_llm_conditional", + 0, + |_request| async { Ok(None) }, + ); ctx.register_llm_request_intercept( "fixture_llm_request_intercept", 0, false, - move |_name, request, annotated| { + move |_name, request, annotated| async move { if llm_request_error { return Err(WorkerSdkError::Callback( "fixture LLM request error requested".into(), diff --git a/crates/core/tests/integration/subscriber_dispatcher_tests.rs b/crates/core/tests/integration/subscriber_dispatcher_tests.rs index 5bdfd7184..27936714d 100644 --- a/crates/core/tests/integration/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/integration/subscriber_dispatcher_tests.rs @@ -12,9 +12,12 @@ use nemo_relay::api::registry::{ deregister_mark_sanitize_guardrail, register_mark_sanitize_guardrail, }; use nemo_relay::api::runtime::{ - NemoRelayContextState, create_scope_stack, global_context, set_thread_scope_stack, + NemoRelayContextState, create_scope_stack, current_scope_stack, global_context, + set_thread_scope_stack, +}; +use nemo_relay::api::scope::{ + EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeType, event, pop_scope, push_scope, }; -use nemo_relay::api::scope::{EmitMarkEventParams, event}; use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber}; use nemo_relay::error::FlowError; use serde_json::json; @@ -103,6 +106,58 @@ fn dispatcher_preserves_event_order() { assert_eq!(observed.lock().unwrap().as_slice(), ["one", "two"]); } +#[test] +fn queued_sanitizer_keeps_the_emission_time_scope_after_pop() { + let _lock = TEST_MUTEX.lock().unwrap(); + flush_subscribers().unwrap(); + reset_global(); + setup_isolated_thread(); + + let scope = push_scope( + PushScopeParams::builder() + .name("emission-scope") + .scope_type(ScopeType::Agent) + .build(), + ) + .unwrap(); + let expected_uuid = scope.uuid; + let (started_tx, started_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); + let release_rx = Arc::new(Mutex::new(release_rx)); + let observed = Arc::new(Mutex::new(None)); + let observed_scope = Arc::clone(&observed); + register_subscriber("scope-snapshot-subscriber", Arc::new(|_| {})).unwrap(); + register_mark_sanitize_guardrail( + "scope-snapshot-sanitizer", + 10, + Arc::new(move |_, fields| { + let release_rx = Arc::clone(&release_rx); + let observed_scope = Arc::clone(&observed_scope); + let started_tx = started_tx.clone(); + Box::pin(async move { + started_tx.send(()).unwrap(); + release_rx.lock().unwrap().recv().unwrap(); + *observed_scope.lock().unwrap() = + Some(current_scope_stack().read().unwrap().top().uuid); + Ok(fields) + }) + }), + ) + .unwrap(); + + emit_mark("scope-snapshot"); + started_rx + .recv_timeout(Duration::from_secs(1)) + .expect("sanitizer should suspend on the dispatcher"); + pop_scope(PopScopeParams::builder().handle_uuid(&scope.uuid).build()).unwrap(); + release_tx.send(()).unwrap(); + flush_subscribers().unwrap(); + + assert_eq!(*observed.lock().unwrap(), Some(expected_uuid)); + deregister_mark_sanitize_guardrail("scope-snapshot-sanitizer").unwrap(); + deregister_subscriber("scope-snapshot-subscriber").unwrap(); +} + #[test] fn dispatcher_continues_after_subscriber_panic() { let _lock = TEST_MUTEX.lock().unwrap(); diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index dbfd1bb14..6b5935b33 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -1063,7 +1063,7 @@ async fn dropping_callback_future_cancels_worker_and_cleans_host_state() { .await; let continuation_id = callback .host_state - .insert_continuation(Continuation::Tool(Arc::new(|value| { + .insert_continuation(Continuation::tool(Arc::new(|value| { Box::pin(async move { Ok(value) }) }))) .expect("continuation should insert"); @@ -1340,7 +1340,11 @@ async fn dropping_host_stream_sends_explicit_worker_cancellation() { } #[tokio::test(flavor = "multi_thread")] +#[allow(clippy::await_holding_lock)] // The process-wide test mutex serializes global registrations. async fn install_registrations_covers_registry_error_edges() { + let _runtime_guard = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); enable_operational_logs(); for surface in [ RegistrationSurface::Subscriber, @@ -1356,15 +1360,17 @@ async fn install_registrations_covers_registry_error_edges() { RegistrationSurface::LlmExecutionIntercept, RegistrationSurface::LlmStreamExecutionIntercept, ] { + let duplicate_name = format!("duplicate_worker_{surface:?}"); let (instance, _shutdown) = fake_worker_instance(vec![ - registration(surface, "duplicate"), - registration(surface, "duplicate"), + registration(surface, &duplicate_name), + registration(surface, &duplicate_name), ]) .await; let mut ctx = PluginRegistrationContext::new(); - let error = instance - .install_registrations(&mut ctx) - .expect_err("duplicate worker registration should fail"); + let error = match instance.install_registrations(&mut ctx) { + Err(error) => error, + Ok(()) => panic!("{surface:?}: duplicate worker registration should fail"), + }; assert!( error.to_string().contains("duplicate") || error.to_string().contains("already registered"), @@ -1968,7 +1974,7 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { }; let llm_continuation = state - .insert_continuation(Continuation::Llm(Arc::new(|request| { + .insert_continuation(Continuation::llm(Arc::new(|request| { Box::pin(async move { Ok(request.content) }) }))) .expect("llm continuation should insert"); @@ -1984,7 +1990,7 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { assert_eq!(wrong_type.code(), tonic::Code::InvalidArgument); let tool_continuation = state - .insert_continuation(Continuation::Tool(Arc::new(|value| { + .insert_continuation(Continuation::tool(Arc::new(|value| { Box::pin(async move { Ok(value) }) }))) .expect("tool continuation should insert"); @@ -2003,7 +2009,7 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { assert_eq!(invalid_tool_json.code(), tonic::Code::InvalidArgument); let llm_continuation = state - .insert_continuation(Continuation::Llm(Arc::new(|request| { + .insert_continuation(Continuation::llm(Arc::new(|request| { Box::pin(async move { Ok(request.content) }) }))) .expect("llm continuation should insert"); @@ -2022,7 +2028,7 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { assert_eq!(invalid_llm_json.code(), tonic::Code::InvalidArgument); let stream_continuation = state - .insert_continuation(Continuation::LlmStream(Arc::new(|_request| { + .insert_continuation(Continuation::llm_stream(Arc::new(|_request| { Box::pin(async move { Ok(LlmJsonStream::new(tokio_stream::iter(vec![Err( FlowError::Internal("stream item failed".into()), @@ -2062,7 +2068,7 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { } let stream_continuation = state - .insert_continuation(Continuation::LlmStream(Arc::new(|_request| { + .insert_continuation(Continuation::llm_stream(Arc::new(|_request| { Box::pin(async move { Ok(LlmJsonStream::new(tokio_stream::empty())) }) }))) .expect("stream continuation should insert"); diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index 202eef880..c3b31886e 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -72,6 +72,15 @@ fn assert_last_error_contains(expected: &str) { ); } +unsafe extern "C" fn accept_native_stream_item( + _user_data: *mut c_void, + _chunk_json: *const NemoRelayNativeString, + _error: *const NemoRelayNativeString, + _done: bool, +) -> bool { + true +} + struct FailingNativeCodec; impl LlmCodec for FailingNativeCodec { @@ -415,6 +424,124 @@ fn native_async_next_is_permanently_one_shot() { } } +#[test] +fn malformed_llm_next_does_not_consume_the_completion() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let next = Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::Llm(Arc::new(|request| { + Box::pin(async move { Ok(request.content) }) + })), + runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, _receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + next_abort: Mutex::new(None), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let invocation = native_string_from_json(&json!({"not": "an llm request"})).unwrap(); + + assert_eq!( + unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, + NemoRelayStatus::InvalidJson + ); + assert!( + completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_some() + ); + assert!(!completion.next_invoked.load(Ordering::SeqCst)); + + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_completion_release(completion_ref); + } +} + +#[test] +fn native_async_stream_next_is_one_shot() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let next = Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + Box::pin(async { Ok(LlmJsonStream::new(tokio_stream::empty())) }) + })), + runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::mpsc::channel(1); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"stream": true}), + }) + .unwrap(), + ) + .unwrap(); + + assert_eq!( + unsafe { + native_async_next_invoke_stream( + next_ref, + invocation, + stream_ref, + accept_native_stream_item, + ptr::null_mut(), + ) + }, + NemoRelayStatus::Ok + ); + assert_eq!( + unsafe { + native_async_next_invoke_stream( + next_ref, + invocation, + stream_ref, + accept_native_stream_item, + ptr::null_mut(), + ) + }, + NemoRelayStatus::InvalidArg + ); + assert_last_error_contains("already invoked"); + + drop(NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }); + runtime.block_on(tokio::task::yield_now()); + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_stream_release(stream_ref); + } +} + #[test] fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlement() { let runtime = tokio::runtime::Builder::new_current_thread() @@ -567,6 +694,7 @@ fn native_async_stream_push_is_bounded_retryable_and_incremental() { let stream = Arc::new(NativeAsyncStream { sender: Mutex::new(Some(sender)), cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), downstream_abort: Mutex::new(None), _callback_user_data: None, }); diff --git a/crates/core/tests/unit/observability/atof_tests.rs b/crates/core/tests/unit/observability/atof_tests.rs index 8452ee407..b1a1bc50a 100644 --- a/crates/core/tests/unit/observability/atof_tests.rs +++ b/crates/core/tests/unit/observability/atof_tests.rs @@ -1618,7 +1618,7 @@ fn http_endpoint_worker_reports_request_transport_failure() { let (close_tx, close_rx) = std::sync::mpsc::channel(); tx.send(EndpointMessage::Close(close_tx)).unwrap(); close_rx - .recv_timeout(std::time::Duration::from_secs(1)) + .recv_timeout(std::time::Duration::from_secs(5)) .unwrap(); worker.join().unwrap(); } diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index a7c81e423..a404ce60e 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -101,11 +101,14 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { } token.scopeStack = null; }; + const safeNext = next === undefined + ? undefined + : (value) => next(jsonValue(value === undefined ? null : value)); const invoke = () => { Promise.resolve().then(() => ( - next === undefined + safeNext === undefined ? (spread ? fn(...arg0) : fn(arg0)) - : (spread ? fn(...arg0, next) : fn(arg0, next)) + : (spread ? fn(...arg0, safeNext) : fn(arg0, safeNext)) )).then((value) => jsonValue(value === undefined ? null : value)).then((value) => { settlePublication(); resolve(value); diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 1e62f1d70..add603697 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -137,15 +137,25 @@ describe('event sanitizer registries', () => { const overrideStack = lib.createScopeStack(); let overrideRootUuid; let emitterScopeUuid; + let emitterScope; const observedParents = []; const observedOverrides = []; + let sanitizerEntered; + const entered = new Promise((resolve) => { + sanitizerEntered = resolve; + }); + let releaseSanitizer; + const release = new Promise((resolve) => { + releaseSanitizer = resolve; + }); lib.registerMarkSanitizeGuardrail('node-event-scope-context', 0, async (event, fields) => { if (event.name !== 'scope-context-original') { return fields; } observedParents.push(lib.getHandle().uuid); - await new Promise((resolve) => setImmediate(resolve)); + sanitizerEntered(); + await release; observedParents.push(lib.getHandle().uuid); lib.event('scope-context-nested', null, { originalParent: event.parent_uuid }); observedOverrides.push( @@ -159,11 +169,15 @@ describe('event sanitizer registries', () => { try { overrideRootUuid = lib.withScopeStack(overrideStack, () => lib.getHandle().uuid); lib.withScopeStack(emitterStack, () => { - emitterScopeUuid = lib.pushScope('scope-context-emitter', lib.ScopeType.Agent).uuid; + emitterScope = lib.pushScope('scope-context-emitter', lib.ScopeType.Agent); + emitterScopeUuid = emitterScope.uuid; lib.event('scope-context-original', null, {}); }); + await entered; + lib.withScopeStack(emitterStack, () => lib.popScope(emitterScope)); lib.setThreadScopeStack(unrelatedStack); const unrelatedRootUuid = lib.getHandle().uuid; + releaseSanitizer(); await lib.flushSubscribers(); await lib.flushSubscribers(); @@ -175,6 +189,7 @@ describe('event sanitizer registries', () => { assert.equal(nested.parent_uuid, emitterScopeUuid); assert.notEqual(nested.parent_uuid, unrelatedRootUuid); } finally { + releaseSanitizer(); lib.setThreadScopeStack(originalStack); lib.deregisterMarkSanitizeGuardrail('node-event-scope-context'); lib.deregisterSubscriber('node-event-sanitize-scope-context-sub'); diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index 335e61fdf..3e37c9d48 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -1364,6 +1364,18 @@ describe('LLM intercepts', () => { ); }); + it('execution intercept rejects non-JSON next arguments without aborting Node', async () => { + registerLlmExecutionIntercept('node_llm_exec_bigint_next', 10, async (_native, next) => next(1n)); + try { + await assert.rejects( + () => llmCallExecute('bigint_next_llm', makeNative(), () => ({ ok: true })), + /unsupported bigint value.*JSON/i, + ); + } finally { + deregisterLlmExecutionIntercept('node_llm_exec_bigint_next'); + } + }); + it('stream execution intercept composes with next', async () => { registerLlmStreamExecutionIntercept('node_llm_stream_exec_repl', 10, async (native, next) => { native.content.intercepted = true; @@ -1415,6 +1427,21 @@ describe('LLM intercepts', () => { deregisterLlmStreamExecutionIntercept('node_llm_stream_exec_repl'); }); + it('stream execution intercept rejects non-JSON next arguments without aborting Node', async () => { + registerLlmStreamExecutionIntercept('node_llm_stream_bigint_next', 10, async (_native, next) => next(1n)); + try { + await assert.rejects( + () => + llmStreamCallExecute('bigint_next_stream_llm', makeNative(), (wrapper) => { + lib.endStream(wrapper.__nemo_relay_stream_id); + }), + /unsupported bigint value.*JSON/i, + ); + } finally { + deregisterLlmStreamExecutionIntercept('node_llm_stream_bigint_next'); + } + }); + it('snapshotted stream execution intercept survives deregistration', async () => { let blockerEntered; const entered = new Promise((resolve) => { diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index 208f5e007..3815b465c 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -989,6 +989,20 @@ describe('Tool intercepts', () => { } }); + it('execution intercept rejects non-JSON next arguments without aborting Node', async () => { + registerToolExecutionIntercept('node_tool_exec_bigint_next', 10, async (_args, next) => ({ + result: await next(1n), + })); + try { + await assert.rejects( + () => toolCallExecute('bigint_next_tool', {}, (args) => args), + /unsupported bigint value.*JSON/i, + ); + } finally { + deregisterToolExecutionIntercept('node_tool_exec_bigint_next'); + } + }); + it('execution intercept next preserves the invocation scope across the chain', async () => { const originalStack = lib.currentScopeStack(); const invocationStack = lib.createScopeStack(); diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index e193a8b42..9ccd34b81 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -30,7 +30,8 @@ use nemo_relay::api::runtime::{ EventSanitizeFn, EventSubscriberFn, LlmConditionalFn, LlmExecutionNextFn, LlmJsonStream, LlmRequestInterceptFn, LlmSanitizeRequestContext, LlmSanitizeRequestFn, LlmSanitizeResponseContext, LlmSanitizeResponseFn, LlmStreamExecutionNextFn, LlmStreamInner, - ToolConditionalFn, ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn, + ToolConditionalFn, ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn, current_scope_stack, + snapshot_scope_stack, }; use nemo_relay::error::{FlowError, Result as FlowResult}; use pyo3::exceptions::PyRuntimeError; @@ -50,7 +51,8 @@ use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; use crate::convert::{json_to_py, py_to_json}; use crate::py_types::{ PyAnnotatedLLMRequest, PyAnnotatedLLMResponse, PyLLMRequest, PyLLMRequestInterceptOutcome, - PyLlmSanitizeRequestContext, PyLlmSanitizeResponseContext, PyToolExecutionInterceptOutcome, + PyLlmSanitizeRequestContext, PyLlmSanitizeResponseContext, PyScopeStack, + PyToolExecutionInterceptOutcome, }; type PyValueFuture = Pin>> + Send>>; @@ -353,6 +355,7 @@ fn capture_python_task_locals() -> Option { struct PythonPublicationContext { task_locals: Option, context: Py, + scope_stack: nemo_relay::api::runtime::ScopeStackHandle, } pub(crate) fn capture_python_publication_context() -> Option { @@ -365,6 +368,7 @@ pub(crate) fn capture_python_publication_context() -> Option Some(Arc::new(PythonPublicationContext { task_locals: pyo3_async_runtimes::tokio::get_current_locals(py).ok(), context, + scope_stack: snapshot_scope_stack(¤t_scope_stack()).ok()?, }) as PublicationContext) }) } @@ -407,6 +411,11 @@ fn copy_publication_invocation<'py>( fallback_task_locals: Option, ) -> PyResult<(Bound<'py, PyAny>, Option)> { let invocation_context = context.context.bind(py).call_method0("copy")?; + let scope_stack = Py::new(py, PyScopeStack(context.scope_stack.clone()))?; + let nemo_relay = py.import("nemo_relay")?; + if let Ok(scope_stack_var) = nemo_relay.getattr("_scope_stack_var") { + invocation_context.call_method1("run", (scope_stack_var.getattr("set")?, scope_stack))?; + } let task_locals = context .task_locals .clone() @@ -877,9 +886,10 @@ pub fn wrap_py_tool_exec_fn( py_fn: Py, ) -> Box Pin> + Send>> + Send + Sync> { let py_fn = std::sync::Arc::new(py_fn); + let registered_task_locals = capture_python_task_locals(); Box::new(move |args: Json| { let py_fn = py_fn.clone(); - let task_locals = task_locals_with_running_loop(None); + let task_locals = task_locals_with_running_loop(registered_task_locals.as_ref()); Box::pin(async move { resolve_json_or_future(Python::attach(|py| { let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) @@ -1366,9 +1376,10 @@ pub fn wrap_py_llm_exec_fn( ) -> Box Pin> + Send>> + Send + Sync> { let py_fn = std::sync::Arc::new(py_fn); + let registered_task_locals = capture_python_task_locals(); Box::new(move |request: LlmRequest| { let py_fn = py_fn.clone(); - let task_locals = task_locals_with_running_loop(None); + let task_locals = task_locals_with_running_loop(registered_task_locals.as_ref()); Box::pin(async move { resolve_json_or_future(Python::attach(|py| { let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) @@ -1403,9 +1414,10 @@ pub fn wrap_py_llm_stream_exec_fn( + Sync, > { let py_fn = std::sync::Arc::new(py_fn); + let registered_task_locals = capture_python_task_locals(); Box::new(move |request: LlmRequest| { let py_fn = py_fn.clone(); - let task_locals = task_locals_with_running_loop(None); + let task_locals = task_locals_with_running_loop(registered_task_locals.as_ref()); Box::pin(async move { let (outcome, invocation_task_locals) = Python::attach(|py| { let (invocation_context, task_locals) = copy_middleware_invocation(py, task_locals) diff --git a/crates/worker/README.md b/crates/worker/README.md index 1f5d81ce3..efa5bb06f 100644 --- a/crates/worker/README.md +++ b/crates/worker/README.md @@ -65,7 +65,7 @@ impl WorkerPlugin for ExampleWorker { } fn register(&self, ctx: &mut PluginContext, _config: &Json) -> Result<()> { - ctx.register_tool_request_intercept("tag-request", 0, false, |_name, mut args| { + ctx.register_tool_request_intercept("tag-request", 0, false, |_name, mut args| async move { if let Some(object) = args.as_object_mut() { object.insert("checked".into(), true.into()); } diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index f2e523f4a..adf80429e 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -132,8 +132,8 @@ type SubscriberFn = Arc; type EventSanitizeFn = Arc BoxFutureResult + Send + Sync>; type ToolSanitizeFn = Arc BoxFutureResult + Send + Sync>; -type ToolConditionalFn = Arc Result> + Send + Sync>; -type ToolRequestFn = Arc Result + Send + Sync>; +type ToolConditionalFn = Arc BoxFutureResult> + Send + Sync>; +type ToolRequestFn = Arc BoxFutureResult + Send + Sync>; type ToolExecutionFn = Arc< dyn Fn(&str, Json, ToolNext) -> BoxFutureResult + Send + Sync, >; @@ -261,9 +261,13 @@ impl WorkerResponseCodec { .await } } -type LlmConditionalFn = Arc Result> + Send + Sync>; +type LlmConditionalFn = Arc BoxFutureResult> + Send + Sync>; type LlmRequestFn = Arc< - dyn Fn(&str, LlmRequest, Option) -> Result + dyn Fn( + String, + LlmRequest, + Option, + ) -> BoxFutureResult + Send + Sync, >; @@ -456,13 +460,14 @@ impl PluginContext { } /// Registers a tool conditional-execution guardrail. - pub fn register_tool_conditional_execution_guardrail( + pub fn register_tool_conditional_execution_guardrail( &mut self, name: &str, priority: i32, callback: F, ) where - F: Fn(&str, &Json) -> Result> + Send + Sync + 'static, + F: Fn(String, Json) -> Fut + Send + Sync + 'static, + Fut: Future>> + Send + 'static, { self.push_registration( name, @@ -470,20 +475,22 @@ impl PluginContext { priority, false, ); - self.handlers - .tool_conditionals - .insert(name.into(), Arc::new(callback)); + self.handlers.tool_conditionals.insert( + name.into(), + Arc::new(move |tool_name, value| Box::pin(callback(tool_name, value))), + ); } /// Registers a tool request intercept. - pub fn register_tool_request_intercept( + pub fn register_tool_request_intercept( &mut self, name: &str, priority: i32, break_chain: bool, callback: F, ) where - F: Fn(&str, Json) -> Result + Send + Sync + 'static, + F: Fn(String, Json) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + 'static, { self.push_registration( name, @@ -491,9 +498,10 @@ impl PluginContext { priority, break_chain, ); - self.handlers - .tool_requests - .insert(name.into(), Arc::new(callback)); + self.handlers.tool_requests.insert( + name.into(), + Arc::new(move |tool_name, value| Box::pin(callback(tool_name, value))), + ); } /// Registers a tool execution intercept. @@ -567,13 +575,14 @@ impl PluginContext { } /// Registers an LLM conditional-execution guardrail. - pub fn register_llm_conditional_execution_guardrail( + pub fn register_llm_conditional_execution_guardrail( &mut self, name: &str, priority: i32, callback: F, ) where - F: Fn(&LlmRequest) -> Result> + Send + Sync + 'static, + F: Fn(LlmRequest) -> Fut + Send + Sync + 'static, + Fut: Future>> + Send + 'static, { self.push_registration( name, @@ -581,23 +590,22 @@ impl PluginContext { priority, false, ); - self.handlers - .llm_conditionals - .insert(name.into(), Arc::new(callback)); + self.handlers.llm_conditionals.insert( + name.into(), + Arc::new(move |request| Box::pin(callback(request))), + ); } /// Registers an LLM request intercept. - pub fn register_llm_request_intercept( + pub fn register_llm_request_intercept( &mut self, name: &str, priority: i32, break_chain: bool, callback: F, ) where - F: Fn(&str, LlmRequest, Option) -> Result - + Send - + Sync - + 'static, + F: Fn(String, LlmRequest, Option) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + 'static, { self.push_registration( name, @@ -605,9 +613,12 @@ impl PluginContext { priority, break_chain, ); - self.handlers - .llm_requests - .insert(name.into(), Arc::new(callback)); + self.handlers.llm_requests.insert( + name.into(), + Arc::new(move |model_name, request, annotated| { + Box::pin(callback(model_name, request, annotated)) + }), + ); } /// Registers an LLM execution intercept. @@ -1711,10 +1722,10 @@ impl WorkerService { .await } RegistrationSurface::ToolConditionalExecutionGuardrail => { - self.invoke_tool_conditional_response(request, scope) + self.invoke_tool_conditional_response(request, scope).await } RegistrationSurface::ToolRequestIntercept => { - self.invoke_tool_request_response(request, scope) + self.invoke_tool_request_response(request, scope).await } RegistrationSurface::ToolExecutionIntercept => { self.invoke_tool_execution_response(request, scope).await @@ -1747,28 +1758,26 @@ impl WorkerService { )) } - fn invoke_tool_conditional_response( + async fn invoke_tool_conditional_response( &self, request: InvokeRequest, scope: &Option, ) -> Result { let payload = tool_payload(request.payload)?; let handler = self.tool_conditional(&request.registration_name)?; - Ok(guardrail_response(with_thread_scope(scope, || { - handler(&payload.tool_name, &payload.value) - })?)) + let future = with_thread_scope(scope, || handler(payload.tool_name, payload.value)); + Ok(guardrail_response(future.await?)) } - fn invoke_tool_request_response( + async fn invoke_tool_request_response( &self, request: InvokeRequest, scope: &Option, ) -> Result { let payload = tool_payload(request.payload)?; let handler = self.tool_request(&request.registration_name)?; - Ok(json_response(with_thread_scope(scope, || { - handler(&payload.tool_name, payload.value) - })?)) + let future = with_thread_scope(scope, || handler(payload.tool_name, payload.value)); + Ok(json_response(future.await?)) } async fn invoke_tool_execution_response( @@ -1802,10 +1811,10 @@ impl WorkerService { .await } RegistrationSurface::LlmConditionalExecutionGuardrail => { - self.invoke_llm_conditional_response(request, scope) + self.invoke_llm_conditional_response(request, scope).await } RegistrationSurface::LlmRequestIntercept => { - self.invoke_llm_request_response(request, scope) + self.invoke_llm_request_response(request, scope).await } RegistrationSurface::LlmExecutionIntercept => { self.invoke_llm_execution_response(request, scope).await @@ -1848,7 +1857,7 @@ impl WorkerService { } } - fn invoke_llm_conditional_response( + async fn invoke_llm_conditional_response( &self, request: InvokeRequest, scope: &Option, @@ -1856,12 +1865,11 @@ impl WorkerService { let payload = llm_payload(request.payload)?; let request_value = required_json::(payload.request, "llm request")?; let handler = self.llm_conditional(&request.registration_name)?; - Ok(guardrail_response(with_thread_scope(scope, || { - handler(&request_value) - })?)) + let future = with_thread_scope(scope, || handler(request_value)); + Ok(guardrail_response(future.await?)) } - fn invoke_llm_request_response( + async fn invoke_llm_request_response( &self, request: InvokeRequest, scope: &Option, @@ -1880,8 +1888,9 @@ impl WorkerService { .transpose()?; let handler = self.llm_request(&request.registration_name)?; let outcome = with_thread_scope(scope, || { - handler(&payload.model_name, request_value, annotated) - })?; + handler(payload.model_name, request_value, annotated) + }) + .await?; llm_request_response(outcome) } diff --git a/crates/worker/tests/worker_sdk_tests.rs b/crates/worker/tests/worker_sdk_tests.rs index 80653155f..d120425b6 100644 --- a/crates/worker/tests/worker_sdk_tests.rs +++ b/crates/worker/tests/worker_sdk_tests.rs @@ -1771,16 +1771,22 @@ impl WorkerPlugin for SurfacePlugin { ctx.register_tool_sanitize_response_guardrail("tool-sanitize", 1, |_, value| async move { Ok(set_json_field(value, "phase", "tool_sanitize_response")) }); - ctx.register_tool_conditional_execution_guardrail("tool-conditional", 1, |_, value| { - Ok(value - .get("block") - .and_then(serde_json::Value::as_bool) - .and_then(|blocked| blocked.then(|| "blocked-tool".into()))) - }); - ctx.register_tool_request_intercept("tool-request", 1, false, |_, value| { + ctx.register_tool_conditional_execution_guardrail( + "tool-conditional", + 1, + |_, value| async move { + tokio::task::yield_now().await; + Ok(value + .get("block") + .and_then(serde_json::Value::as_bool) + .and_then(|blocked| blocked.then(|| "blocked-tool".into()))) + }, + ); + ctx.register_tool_request_intercept("tool-request", 1, false, |_, value| async move { + tokio::task::yield_now().await; Ok(set_json_field(value, "phase", "tool_request")) }); - ctx.register_tool_request_intercept("tool-error", 1, false, |_, _| { + ctx.register_tool_request_intercept("tool-error", 1, false, |_, _| async { Err(WorkerSdkError::Callback("boom".into())) }); @@ -1885,19 +1891,30 @@ impl WorkerPlugin for SurfacePlugin { 1, |_response, _context| async { Ok(None) }, ); - ctx.register_llm_conditional_execution_guardrail("llm-conditional", 1, |request| { - Ok(request - .content - .get("block") - .and_then(serde_json::Value::as_bool) - .and_then(|blocked| blocked.then(|| "blocked-llm".into()))) - }); - ctx.register_llm_request_intercept("llm-request", 1, false, |_, request, annotated| { - Ok(nemo_relay_worker::LlmRequestInterceptOutcome::new( - set_llm_phase(request, "llm_request"), - annotated, - )) - }); + ctx.register_llm_conditional_execution_guardrail( + "llm-conditional", + 1, + |request| async move { + tokio::task::yield_now().await; + Ok(request + .content + .get("block") + .and_then(serde_json::Value::as_bool) + .and_then(|blocked| blocked.then(|| "blocked-llm".into()))) + }, + ); + ctx.register_llm_request_intercept( + "llm-request", + 1, + false, + |_, request, annotated| async move { + tokio::task::yield_now().await; + Ok(nemo_relay_worker::LlmRequestInterceptOutcome::new( + set_llm_phase(request, "llm_request"), + annotated, + )) + }, + ); ctx.register_llm_execution_intercept( "llm-exec", diff --git a/python/tests/test_dynamic_plugin_host.py b/python/tests/test_dynamic_plugin_host.py index e0d6539e2..2f5ea351b 100644 --- a/python/tests/test_dynamic_plugin_host.py +++ b/python/tests/test_dynamic_plugin_host.py @@ -22,7 +22,7 @@ import pytest -from nemo_relay import Json, plugin, scope, tools +from nemo_relay import Json, LLMRequest, llm, plugin, scope, tools @dataclass(frozen=True, slots=True) @@ -534,10 +534,55 @@ async def test_worker_activation_finalizer_never_waits_on_python_thread( async def test_worker_activation_executes_and_releases_callbacks(worker_dynamic_plugin: _BuiltPlugin): activation = await plugin.initialize_with_dynamic_plugins({}, [worker_dynamic_plugin.spec()]) + loop = asyncio.get_running_loop() + loop_thread = threading.get_ident() + + async def tool_provider(args: Json) -> Json: + assert asyncio.get_running_loop() is loop + assert threading.get_ident() == loop_thread + await asyncio.sleep(0) + return {"args": args} + + async def llm_provider(request: LLMRequest) -> Json: + assert asyncio.get_running_loop() is loop + assert threading.get_ident() == loop_thread + await asyncio.sleep(0) + return {"request": request.content} + + async def stream_provider(request: LLMRequest): + assert asyncio.get_running_loop() is loop + assert threading.get_ident() == loop_thread + await asyncio.sleep(0) + yield {"request": request.content} + try: - result = await tools.execute("python-worker-fixture", {"input": True}, lambda args: {"args": args}) + result = await tools.execute("python-worker-fixture", {"input": True}, tool_provider) assert result["worker_plugin_tool_execution"] is True assert result["args"]["worker_plugin_tool_execution_request"] is True + + llm_result = await llm.execute( + "python-worker-llm", + LLMRequest({}, {"model": "worker"}), + llm_provider, + ) + assert llm_result["worker_plugin_llm_execution"] is True + assert llm_result["request"]["worker_plugin_llm_execution_request"] is True + + stream = await llm.stream_execute( + "python-worker-stream", + LLMRequest({}, {"model": "worker"}), + stream_provider, + lambda _chunk: None, + lambda: {}, + ) + chunks = [chunk async for chunk in stream] + assert chunks + chunk = chunks[0] + assert isinstance(chunk, dict) + assert chunk["worker_plugin_llm_stream_execution"] is True + request = chunk["request"] + assert isinstance(request, dict) + assert request["worker_plugin_llm_stream_execution_request"] is True finally: await activation.close() diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index a3dcf3fd8..18acbfdec 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -257,6 +257,39 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert events[-1].data == {"async_flush": True} +async def test_queued_sanitizer_keeps_emission_scope_after_pop(capture_events): + _capture_name, events = capture_events + handle = scope.push("python-emission-scope", nemo_relay.ScopeType.Agent) + entered = asyncio.Event() + release = asyncio.Event() + observed: list[str] = [] + + async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + if event.name != "python-scope-snapshot": + return fields + entered.set() + await release.wait() + observed.append(scope.get_handle().uuid) + scope.event("python-scope-snapshot-nested") + return fields + + guardrails.register_mark_sanitize("python-scope-snapshot", 0, sanitize) + try: + scope.event("python-scope-snapshot") + await asyncio.wait_for(entered.wait(), timeout=1) + scope.pop(handle) + release.set() + await asyncio.wait_for(subscribers.flush_async(), timeout=2) + await asyncio.wait_for(subscribers.flush_async(), timeout=2) + finally: + release.set() + guardrails.deregister_mark_sanitize("python-scope-snapshot") + + assert observed == [handle.uuid] + nested = next(event for event in events if event.name == "python-scope-snapshot-nested") + assert nested.parent_uuid == handle.uuid + + async def test_async_flush_does_not_consume_default_executor(capture_events): _capture_name, events = capture_events asyncio.get_running_loop().set_default_executor(ThreadPoolExecutor(max_workers=1)) From 603dee9ed92dc4065d2f9065875d8e127c7c4253 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 07:31:35 -0400 Subject: [PATCH 52/83] fix: preserve async middleware lifecycle context Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 37 +++-- crates/core/src/api/scope.rs | 5 +- crates/core/src/plugin/dynamic/native.rs | 23 +-- crates/core/src/plugin/dynamic/worker.rs | 32 +++- .../tests/fixtures/native_plugin/src/lib.rs | 96 +++++------ .../tests/integration/middleware_tests.rs | 85 ++++++++++ .../subscriber_dispatcher_tests.rs | 44 +++++- .../core/tests/unit/dynamic_worker_tests.rs | 30 ++++ crates/core/tests/unit/native_plugin_tests.rs | 149 ++++++++++++++++++ crates/node/tests/event_sanitizers_tests.mjs | 25 +++ crates/plugin/src/lib.rs | 50 ++++-- .../about-nemo-relay/concepts/subscribers.mdx | 2 +- 12 files changed, 475 insertions(+), 103 deletions(-) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 42e9e39dd..ae1a943b3 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -330,7 +330,7 @@ mod native { scope_stack, publication_context: current_publication_context(), }; - send_dispatch_message(message) + enqueue_dispatch_message(message) } pub(super) fn dispatch_reserved_sanitized_event( @@ -353,21 +353,7 @@ mod native { scope_stack, publication_context: current_publication_context(), }; - let buffer_active = ASYNC_PUBLICATION_MESSAGES - .try_with(|messages| messages.borrow().is_some()) - .unwrap_or(false); - if buffer_active { - ASYNC_PUBLICATION_MESSAGES.with(|messages| { - messages - .borrow_mut() - .as_mut() - .expect("publication buffer checked above") - .push(message); - }); - true - } else { - send_dispatch_message(message) - } + enqueue_dispatch_message(message) } pub(super) fn dispatch_transformed_event( @@ -388,7 +374,7 @@ mod native { scope_stack, publication_context: current_publication_context(), }; - send_dispatch_message(message) + enqueue_dispatch_message(message) } /// Reserve a FIFO position for publications produced by an async task. @@ -501,6 +487,23 @@ mod native { } } + fn enqueue_dispatch_message(message: DispatcherMessage) -> bool { + let mut message = Some(message); + let buffered = ASYNC_PUBLICATION_MESSAGES + .try_with(|messages| { + let mut messages = messages.borrow_mut(); + match messages.as_mut() { + Some(messages) => { + messages.push(message.take().expect("message is buffered once")); + true + } + None => false, + } + }) + .unwrap_or(false); + buffered || send_dispatch_message(message.expect("unbuffered message remains available")) + } + fn start_dispatcher() -> std::result::Result, String> { let (tx, rx) = mpsc::channel::(); let sender = std::thread::Builder::new() diff --git a/crates/core/src/api/scope.rs b/crates/core/src/api/scope.rs index dd5cf9154..2e3dbd49a 100644 --- a/crates/core/src/api/scope.rs +++ b/crates/core/src/api/scope.rs @@ -5,7 +5,7 @@ use crate::api::event::{BaseEvent, CategoryProfile, DataSchema, EventCategory, M use crate::api::runtime::global_context; use crate::api::runtime::subscriber_dispatcher; use crate::api::runtime::{ - current_scope_stack, task_scope_push, task_scope_remove, task_scope_top, + current_scope_stack, snapshot_scope_stack, task_scope_push, task_scope_remove, task_scope_top, }; use crate::api::shared::{ ensure_runtime_owner, resolve_parent_uuid, snapshot_event_sanitizers, @@ -331,13 +331,14 @@ pub fn pop_scope(params: PopScopeParams<'_>) -> Result<()> { // published later, but scope cleanup must not change the middleware that // was visible when the end event was emitted. let sanitizers = snapshot_event_sanitizers(&event, &emission_scope_stack).unwrap_or_default(); + let publication_scope_stack = snapshot_scope_stack(&emission_scope_stack)?; let removed = task_scope_remove(params.handle_uuid)?; debug_assert_eq!(removed.uuid, scope.uuid); let _ = subscriber_dispatcher::dispatch_sanitized_event( event, sanitizers, &subscribers, - emission_scope_stack, + publication_scope_stack, ); Ok(()) } diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 0eae9c148..07f3dd2ac 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -1445,6 +1445,7 @@ struct NativeAsyncStreamReceiver { struct NativeAsyncStreamCallbackGuard { cb: NemoRelayNativeAsyncNextStreamCb, user_data: usize, + stream: Arc, active: bool, } @@ -1456,7 +1457,7 @@ impl NativeAsyncStreamCallbackGuard { impl Drop for NativeAsyncStreamCallbackGuard { fn drop(&mut self) { - if self.active { + if self.active && !self.stream.cancelled.load(Ordering::Acquire) { unsafe { let _ = (self.cb)( self.user_data as *mut c_void, @@ -1930,6 +1931,7 @@ unsafe extern "C" fn native_async_next_invoke_stream( let scope_stack = next.scope_stack.clone(); let library_guard = next._callback_user_data.clone(); let user_data = user_data as usize; + let output_stream_for_task = Arc::clone(&output_stream); let task = next .runtime .spawn(TASK_SCOPE_STACK.scope(scope_stack, async move { @@ -1937,6 +1939,7 @@ unsafe extern "C" fn native_async_next_invoke_stream( let mut callback_guard = NativeAsyncStreamCallbackGuard { cb, user_data, + stream: output_stream_for_task, active: true, }; match next_fn(request).await { @@ -1952,14 +1955,6 @@ unsafe extern "C" fn native_async_next_invoke_stream( native_string_free(chunk); } if !keep_going { - unsafe { - let _ = cb( - user_data as *mut c_void, - ptr::null(), - ptr::null(), - true, - ); - } callback_guard.finish(); return; } @@ -2000,10 +1995,16 @@ unsafe extern "C" fn native_async_next_invoke_stream( } } })); - *output_stream + let abort = task.abort_handle(); + let mut downstream_abort = output_stream .downstream_abort .lock() - .unwrap_or_else(|error| error.into_inner()) = Some(task.abort_handle()); + .unwrap_or_else(|error| error.into_inner()); + if output_stream.cancelled.load(Ordering::Acquire) { + abort.abort(); + } else { + *downstream_abort = Some(abort); + } NemoRelayStatus::Ok } diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index 3a63e94f5..b1cc8309e 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -58,6 +58,9 @@ use tower::service_fn; use crate::api::event::{Event, EventSanitizeFields}; use crate::api::llm::{LLM_REQUEST_INTERCEPT_OUTCOME_SCHEMA, LlmRequest}; +use crate::api::optimization::{ + LlmOptimizationRecorder, current_llm_optimization_recorder, scope_llm_optimization_recorder, +}; use crate::api::runtime::scope_stack::active_event_uuid; use crate::api::runtime::subscriber_dispatcher::{ PublicationContext, capture_publication_context, with_task_publication_context, @@ -2402,6 +2405,7 @@ struct ContinuationContext { scope_stack: crate::api::runtime::ScopeStackHandle, active_event_uuid: Option, publication_context: Option, + optimization_recorder: Option, } impl ContinuationContext { @@ -2410,15 +2414,22 @@ impl ContinuationContext { scope_stack: current_scope_stack(), active_event_uuid: active_event_uuid(), publication_context: capture_publication_context(), + optimization_recorder: current_llm_optimization_recorder(), } } async fn run(&self, future: F) -> F::Output { let scoped = TASK_SCOPE_STACK.scope(self.scope_stack.clone(), future); let published = with_task_publication_context(self.publication_context.clone(), scoped); - match self.active_event_uuid { - Some(uuid) => with_active_event_uuid(uuid, published).await, - None => published.await, + let active = async { + match self.active_event_uuid { + Some(uuid) => with_active_event_uuid(uuid, published).await, + None => published.await, + } + }; + match &self.optimization_recorder { + Some(recorder) => scope_llm_optimization_recorder(recorder.clone(), active).await, + None => active.await, } } } @@ -2684,9 +2695,18 @@ impl RelayHostRuntime for WorkerHostRuntimeService { context .run(async move { let mut stream = stream; - while let Some(item) = stream.next().await { - if tx.send(item).await.is_err() { - break; + loop { + tokio::select! { + biased; + _ = tx.closed() => break, + item = stream.next() => { + let Some(item) = item else { + break; + }; + if tx.send(item).await.is_err() { + break; + } + } } } }) diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index da5752fb7..28f4e7047 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -303,26 +303,13 @@ pub unsafe extern "C" fn nemo_relay_fixture_async_entry( host: *const NemoRelayNativeHostApiV1, out: *mut NemoRelayNativePluginV1, ) -> NemoRelayStatus { - if host.is_null() || out.is_null() { - return NemoRelayStatus::NullPointer; - } - let host_v1 = unsafe { &*host }; - if host_v1.abi_version < 3 - || host_v1.struct_size < std::mem::size_of::() - { - return NemoRelayStatus::InvalidArg; - } - let host_v3 = unsafe { &*(host as *const NemoRelayNativeHostApiV3) }; - let mut plugin = NemoRelayNativePluginV1::default(); - plugin.plugin_kind = unsafe { raw_host_string(&host_v3.v1, "fixture_async") }; - if plugin.plugin_kind.is_null() { - return NemoRelayStatus::Internal; + unsafe { + nemo_relay_plugin::export_plugin( + host, + out, + FixtureAsyncPlugin { host: None }, + ) } - plugin.user_data = Box::into_raw(Box::new(*host_v3)).cast(); - plugin.register = Some(raw_register_async_tool_request); - plugin.drop = Some(raw_drop_async_host); - unsafe { *out = plugin }; - NemoRelayStatus::Ok } #[unsafe(no_mangle)] @@ -615,15 +602,35 @@ unsafe extern "C" fn raw_register_event_sanitize_errors( status } -unsafe extern "C" fn raw_register_async_tool_request( - user_data: *mut c_void, - _plugin_config_json: *const NemoRelayNativeString, - ctx: *mut NemoRelayNativePluginContext, -) -> NemoRelayStatus { - if user_data.is_null() { - return NemoRelayStatus::NullPointer; +struct FixtureAsyncPlugin { + host: Option>, +} + +impl NativePlugin for FixtureAsyncPlugin { + fn plugin_kind(&self) -> &str { + "fixture_async" } - let host = unsafe { &*(user_data as *const NemoRelayNativeHostApiV3) }; + + fn register( + &mut self, + _plugin_config: &Map, + ctx: &mut PluginContext<'_>, + ) -> nemo_relay_plugin::Result<()> { + let host = ctx.host_api(); + if host.abi_version < 3 + || host.struct_size < std::mem::size_of::() + { + return Err("fixture async plugin requires ABI v3".into()); + } + self.host = Some(Box::new(unsafe { + *(host as *const _ as *const NemoRelayNativeHostApiV3) + })); + let user_data = self + .host + .as_deref() + .map(|host| (host as *const NemoRelayNativeHostApiV3).cast_mut().cast()) + .expect("fixture async host was initialized"); + let registrations: [( NemoRelayNativeAsyncMiddlewareKind, &str, @@ -696,15 +703,10 @@ unsafe extern "C" fn raw_register_async_tool_request( ), ]; for (kind, registration_name, callback) in registrations { - let name = unsafe { raw_host_string(&host.v1, registration_name) }; - if name.is_null() { - return NemoRelayStatus::Internal; - } let status = unsafe { - (host.plugin_context_register_async_middleware)( - ctx, - kind as u32, - name, + ctx.register_async_middleware_raw( + kind, + registration_name, 0, false, callback, @@ -712,30 +714,24 @@ unsafe extern "C" fn raw_register_async_tool_request( None, ) }; - unsafe { (host.v1.string_free)(name) }; if status != NemoRelayStatus::Ok { - return status; + return Err(format!("async registration failed: {status:?}")); } } - let name = unsafe { raw_host_string(&host.v1, "fixture_async_llm_stream") }; - if name.is_null() { - return NemoRelayStatus::Internal; - } let status = unsafe { - (host.plugin_context_register_async_stream_middleware)( - ctx, - name, + ctx.register_async_stream_middleware_raw( + "fixture_async_llm_stream", 0, raw_async_stream_callback, user_data, None, ) }; - unsafe { (host.v1.string_free)(name) }; if status != NemoRelayStatus::Ok { - return status; + return Err(format!("async stream registration failed: {status:?}")); + } + Ok(()) } - NemoRelayStatus::Ok } struct AsyncStreamForward { @@ -1149,12 +1145,6 @@ unsafe extern "C" fn raw_drop_host(user_data: *mut c_void) { } } -unsafe extern "C" fn raw_drop_async_host(user_data: *mut c_void) { - if !user_data.is_null() { - drop(unsafe { Box::from_raw(user_data as *mut NemoRelayNativeHostApiV3) }); - } -} - unsafe fn raw_host_from_user_data<'a>( user_data: *mut c_void, ) -> Option<&'a NemoRelayNativeHostApiV1> { diff --git a/crates/core/tests/integration/middleware_tests.rs b/crates/core/tests/integration/middleware_tests.rs index 053cba446..9a8a205af 100644 --- a/crates/core/tests/integration/middleware_tests.rs +++ b/crates/core/tests/integration/middleware_tests.rs @@ -3600,6 +3600,91 @@ async fn test_managed_llm_event_sanitizers_run_off_execution_path_in_fifo_order( deregister_subscriber("managed_async_publication_observer").unwrap(); } +#[tokio::test] +async fn test_stream_response_sanitizer_nested_mark_precedes_end_event() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = Arc::clone(&events); + register_subscriber( + "stream_nested_publication_observer", + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .unwrap(); + register_llm_sanitize_response_guardrail( + "stream_nested_publication_sanitizer", + 1, + Arc::new(|response, _context| { + Box::pin(async move { + tokio::task::yield_now().await; + event( + EmitMarkEventParams::builder() + .name("stream-sanitizer-nested-mark") + .build(), + ) + .unwrap(); + Ok(Some(response)) + }) + }), + ) + .unwrap(); + + let mut stream = llm_stream_call_execute( + LlmStreamCallExecuteParams::builder() + .name("stream-nested-publication") + .request(LlmRequest { + headers: serde_json::Map::new(), + content: json!({"prompt": "hello"}), + }) + .func(Arc::new(|_| { + Box::pin(async { + Ok(LlmJsonStream::new(tokio_stream::iter(vec![Ok(json!({ + "chunk": "done" + }))]))) + }) + })) + .collector(Box::new(|_| Ok(()))) + .finalizer(Box::new(|| json!({"response": "done"}))) + .build(), + ) + .await + .unwrap(); + while let Some(item) = stream.next().await { + item.unwrap(); + } + stream.close().await.unwrap(); + flush_subscribers().unwrap(); + + let names = events + .lock() + .unwrap() + .iter() + .map(|event| { + ( + event.name().to_string(), + event.scope_category(), + event.parent_uuid(), + ) + }) + .collect::>(); + let mark_index = names + .iter() + .position(|(name, _, _)| name == "stream-sanitizer-nested-mark") + .unwrap(); + let end_index = names + .iter() + .position(|(name, category, _)| { + name == "stream-nested-publication" && *category == Some(ScopeCategory::End) + }) + .unwrap(); + assert!(mark_index < end_index); + + deregister_llm_sanitize_response_guardrail("stream_nested_publication_sanitizer").unwrap(); + deregister_subscriber("stream_nested_publication_observer").unwrap(); +} + #[tokio::test] async fn test_managed_llm_emits_pending_marks_under_started_scope() { let _lock = TEST_MUTEX.lock().unwrap(); diff --git a/crates/core/tests/integration/subscriber_dispatcher_tests.rs b/crates/core/tests/integration/subscriber_dispatcher_tests.rs index 27936714d..b874d7ef6 100644 --- a/crates/core/tests/integration/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/integration/subscriber_dispatcher_tests.rs @@ -9,7 +9,8 @@ use std::time::Duration; use nemo_relay::api::event::Event; use nemo_relay::api::registry::{ - deregister_mark_sanitize_guardrail, register_mark_sanitize_guardrail, + deregister_mark_sanitize_guardrail, deregister_scope_sanitize_end_guardrail, + register_mark_sanitize_guardrail, register_scope_sanitize_end_guardrail, }; use nemo_relay::api::runtime::{ NemoRelayContextState, create_scope_stack, current_scope_stack, global_context, @@ -158,6 +159,47 @@ fn queued_sanitizer_keeps_the_emission_time_scope_after_pop() { deregister_subscriber("scope-snapshot-subscriber").unwrap(); } +#[test] +fn scope_end_sanitizer_keeps_the_ending_scope_across_await() { + let _lock = TEST_MUTEX.lock().unwrap(); + flush_subscribers().unwrap(); + reset_global(); + setup_isolated_thread(); + + let scope = push_scope( + PushScopeParams::builder() + .name("ending-scope") + .scope_type(ScopeType::Agent) + .build(), + ) + .unwrap(); + let expected_uuid = scope.uuid; + let observed = Arc::new(Mutex::new(None)); + let observed_scope = Arc::clone(&observed); + register_subscriber("scope-end-context-subscriber", Arc::new(|_| {})).unwrap(); + register_scope_sanitize_end_guardrail( + "scope-end-context-sanitizer", + 10, + Arc::new(move |_, fields| { + let observed_scope = Arc::clone(&observed_scope); + Box::pin(async move { + tokio::task::yield_now().await; + *observed_scope.lock().unwrap() = + Some(current_scope_stack().read().unwrap().top().uuid); + Ok(fields) + }) + }), + ) + .unwrap(); + + pop_scope(PopScopeParams::builder().handle_uuid(&scope.uuid).build()).unwrap(); + flush_subscribers().unwrap(); + + assert_eq!(*observed.lock().unwrap(), Some(expected_uuid)); + deregister_scope_sanitize_end_guardrail("scope-end-context-sanitizer").unwrap(); + deregister_subscriber("scope-end-context-subscriber").unwrap(); +} + #[test] fn dispatcher_continues_after_subscriber_panic() { let _lock = TEST_MUTEX.lock().unwrap(); diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index 6b5935b33..481f4502a 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -4,11 +4,15 @@ use std::sync::{Arc, Mutex}; use crate::api::event::{BaseEvent, MarkEvent}; +use crate::api::optimization::{ + LlmOptimizationRecorder, record_llm_optimization_contribution, scope_llm_optimization_recorder, +}; use crate::api::runtime::{ BuiltinLlmCodec, LlmCodecIdentity, LlmSanitizeRequestContext, LlmSanitizeResponseContext, NemoRelayContextState, }; use crate::codec::openai_chat::OpenAIChatCodec; +use crate::codec::optimization::LlmOptimizationContribution; use crate::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_relay_worker_proto::json_envelope; use nemo_relay_worker_proto::v1::invoke_response::Result as InvokeResult; @@ -38,6 +42,32 @@ fn enable_operational_logs() { log::set_max_level(log::LevelFilter::Info); } +#[tokio::test] +async fn continuation_context_preserves_optimization_recorder_across_tasks() { + for producer in ["worker-unary-next", "worker-stream-next"] { + let recorder = LlmOptimizationRecorder::default(); + let context = scope_llm_optimization_recorder(recorder.clone(), async { + ContinuationContext::capture() + }) + .await; + tokio::spawn(async move { + context + .run(async move { + tokio::task::yield_now().await; + assert!(record_llm_optimization_contribution( + LlmOptimizationContribution::new(producer, "worker_next") + )); + }) + .await; + }) + .await + .unwrap(); + let contributions = recorder.unemitted(); + assert_eq!(contributions.len(), 1); + assert_eq!(contributions[0].producer, producer); + } +} + #[test] fn python_environment_resolution_requires_lifecycle_managed_path() { enable_operational_logs(); diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index c3b31886e..a89d75961 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -81,6 +81,17 @@ unsafe extern "C" fn accept_native_stream_item( true } +unsafe extern "C" fn stop_after_first_native_stream_item( + user_data: *mut c_void, + _chunk_json: *const NemoRelayNativeString, + _error: *const NemoRelayNativeString, + _done: bool, +) -> bool { + let callbacks = unsafe { &*(user_data as *const AtomicUsize) }; + callbacks.fetch_add(1, Ordering::SeqCst); + false +} + struct FailingNativeCodec; impl LlmCodec for FailingNativeCodec { @@ -542,6 +553,144 @@ fn native_async_stream_next_is_one_shot() { } } +#[test] +fn native_async_stream_next_stops_callbacks_after_false() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let next = Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + Box::pin(async { + Ok(LlmJsonStream::new(tokio_stream::iter(vec![ + Ok(json!({"chunk": 1})), + Ok(json!({"chunk": 2})), + ]))) + }) + })), + runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::mpsc::channel(1); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"stream": true}), + }) + .unwrap(), + ) + .unwrap(); + let callbacks = AtomicUsize::new(0); + + assert_eq!( + unsafe { + native_async_next_invoke_stream( + next_ref, + invocation, + stream_ref, + stop_after_first_native_stream_item, + (&callbacks as *const AtomicUsize).cast_mut().cast(), + ) + }, + NemoRelayStatus::Ok + ); + runtime.block_on(async { + for _ in 0..10 { + tokio::task::yield_now().await; + } + }); + assert_eq!(callbacks.load(Ordering::SeqCst), 1); + + drop(NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }); + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_stream_release(stream_ref); + } +} + +#[test] +fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let started_tx = Arc::new(Mutex::new(Some(started_tx))); + let next = Arc::new(NativeAsyncNext { + inner: NativeAsyncNextInner::LlmStream(Arc::new(move |_request| { + let started_tx = Arc::clone(&started_tx); + Box::pin(async move { + if let Some(started_tx) = started_tx.lock().unwrap().take() { + let _ = started_tx.send(()); + } + std::future::pending().await + }) + })), + runtime: runtime.handle().clone(), + scope_stack: current_scope_stack(), + _callback_user_data: None, + }); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::mpsc::channel(1); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"stream": true}), + }) + .unwrap(), + ) + .unwrap(); + let callbacks = AtomicUsize::new(0); + + assert_eq!( + unsafe { + native_async_next_invoke_stream( + next_ref, + invocation, + stream_ref, + stop_after_first_native_stream_item, + (&callbacks as *const AtomicUsize).cast_mut().cast(), + ) + }, + NemoRelayStatus::Ok + ); + runtime.block_on(started_rx).unwrap(); + drop(NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }); + runtime.block_on(tokio::task::yield_now()); + assert_eq!(callbacks.load(Ordering::SeqCst), 0); + + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_stream_release(stream_ref); + } +} + #[test] fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlement() { let runtime = tokio::runtime::Builder::new_current_thread() diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index add603697..a60b67454 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -196,6 +196,31 @@ describe('event sanitizer registries', () => { } }); + it('preserves the ending scope across an async scope-end sanitizer', async () => { + const events = capture('node-scope-end-context-sub'); + const observed = []; + lib.registerScopeSanitizeEndGuardrail( + 'node-scope-end-context', + 0, + async (_event, fields) => { + observed.push(lib.getHandle().uuid); + await new Promise((resolve) => setImmediate(resolve)); + observed.push(lib.getHandle().uuid); + return fields; + }, + ); + const scope = lib.pushScope('node-ending-scope', lib.ScopeType.Agent); + try { + lib.popScope(scope); + await lib.flushSubscribers(); + await waitFor(events, 2); + } finally { + lib.deregisterScopeSanitizeEndGuardrail('node-scope-end-context'); + lib.deregisterSubscriber('node-scope-end-context-sub'); + } + assert.deepEqual(observed, [scope.uuid, scope.uuid]); + }); + it('preserves snapshotted sanitizers after deregistration', async () => { const events = capture('node-event-sanitize-snapshot-sub'); let blockerEntered; diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 79e4da5ba..817af4b4f 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -3230,15 +3230,39 @@ impl<'a> OptionalHostJson<'a> { } } +enum OwnedHostApi { + V1(NemoRelayNativeHostApiV1), + V3(NemoRelayNativeHostApiV3), +} + +impl OwnedHostApi { + unsafe fn copy_from(host: &NemoRelayNativeHostApiV1) -> Self { + if host.abi_version >= NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE + && host.struct_size >= std::mem::size_of::() + { + Self::V3(unsafe { *(host as *const _ as *const NemoRelayNativeHostApiV3) }) + } else { + Self::V1(*host) + } + } + + fn v1(&self) -> &NemoRelayNativeHostApiV1 { + match self { + Self::V1(host) => host, + Self::V3(host) => &host.v1, + } + } +} + struct PluginState

{ - host: NemoRelayNativeHostApiV1, + host: OwnedHostApi, plugin: Mutex

, } unsafe extern "C" fn drop_plugin_state(user_data: *mut c_void) { if !user_data.is_null() { let state = unsafe { Box::from_raw(user_data as *mut PluginState

) }; - let host = state.host; + let host = *state.host.v1(); if catch_unwind(AssertUnwindSafe(|| drop(state))).is_err() { set_last_error(&host, "native plugin state drop panicked"); } @@ -3256,22 +3280,23 @@ unsafe extern "C" fn validate_trampoline( unsafe { *out_diagnostics_json = ptr::null_mut() }; let state = unsafe { &*(user_data as *const PluginState

) }; let result = catch_unwind(AssertUnwindSafe(|| { - let config = match read_json_object(&state.host, plugin_config_json) { + let host = state.host.v1(); + let config = match read_json_object(host, plugin_config_json) { Ok(config) => config, Err(status) => return status, }; let plugin = match state.plugin.lock() { Ok(plugin) => plugin, Err(_) => { - set_last_error(&state.host, "native plugin state lock poisoned"); + set_last_error(host, "native plugin state lock poisoned"); return NemoRelayStatus::Internal; } }; let diagnostics = plugin.validate(&config); - write_json(&state.host, &diagnostics, out_diagnostics_json) + write_json(host, &diagnostics, out_diagnostics_json) })); result.unwrap_or_else(|_| { - set_last_error(&state.host, "native plugin validate callback panicked"); + set_last_error(state.host.v1(), "native plugin validate callback panicked"); NemoRelayStatus::Internal }) } @@ -3286,28 +3311,29 @@ unsafe extern "C" fn register_trampoline( } let state = unsafe { &*(user_data as *const PluginState

) }; let result = catch_unwind(AssertUnwindSafe(|| { - let config = match read_json_object(&state.host, plugin_config_json) { + let host = state.host.v1(); + let config = match read_json_object(host, plugin_config_json) { Ok(config) => config, Err(status) => return status, }; - let mut ctx = unsafe { PluginContext::from_raw(&state.host, ctx) }; + let mut ctx = unsafe { PluginContext::from_raw(host, ctx) }; let mut plugin = match state.plugin.lock() { Ok(plugin) => plugin, Err(_) => { - set_last_error(&state.host, "native plugin state lock poisoned"); + set_last_error(host, "native plugin state lock poisoned"); return NemoRelayStatus::Internal; } }; match plugin.register(&config, &mut ctx) { Ok(()) => NemoRelayStatus::Ok, Err(message) => { - set_last_error(&state.host, &message); + set_last_error(host, &message); NemoRelayStatus::Internal } } })); result.unwrap_or_else(|_| { - set_last_error(&state.host, "native plugin register callback panicked"); + set_last_error(state.host.v1(), "native plugin register callback panicked"); NemoRelayStatus::Internal }) } @@ -3551,7 +3577,7 @@ where return NemoRelayStatus::Internal; }; let state = Box::new(PluginState { - host: *host_ref, + host: unsafe { OwnedHostApi::copy_from(host_ref) }, plugin: Mutex::new(plugin), }); unsafe { diff --git a/docs/about-nemo-relay/concepts/subscribers.mdx b/docs/about-nemo-relay/concepts/subscribers.mdx index 15a205d17..d24032e39 100644 --- a/docs/about-nemo-relay/concepts/subscribers.mdx +++ b/docs/about-nemo-relay/concepts/subscribers.mdx @@ -151,7 +151,7 @@ observe side effects from callbacks that were already queued before the barrier: - Rust: `nemo_relay::api::subscriber::flush_subscribers()?` - Python: `nemo_relay.subscribers.flush()` from synchronous code, or `await nemo_relay.subscribers.flush_async()` from an `asyncio` task -- Node.js: `flushSubscribers()`, then await an event-loop tick for JavaScript +- Node.js: `await flushSubscribers()`, then await an event-loop tick for JavaScript callback side effects - FFI: `nemo_relay_flush_subscribers()` From cefed3cf19f000e5949f7ec66926618d1ed3d6be Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 07:45:55 -0400 Subject: [PATCH 53/83] style(plugin): format native async fixture Signed-off-by: Will Killian --- .../tests/fixtures/native_plugin/src/lib.rs | 190 +++++++++--------- 1 file changed, 92 insertions(+), 98 deletions(-) diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index 28f4e7047..6af594610 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -303,13 +303,7 @@ pub unsafe extern "C" fn nemo_relay_fixture_async_entry( host: *const NemoRelayNativeHostApiV1, out: *mut NemoRelayNativePluginV1, ) -> NemoRelayStatus { - unsafe { - nemo_relay_plugin::export_plugin( - host, - out, - FixtureAsyncPlugin { host: None }, - ) - } + unsafe { nemo_relay_plugin::export_plugin(host, out, FixtureAsyncPlugin { host: None }) } } #[unsafe(no_mangle)] @@ -631,105 +625,105 @@ impl NativePlugin for FixtureAsyncPlugin { .map(|host| (host as *const NemoRelayNativeHostApiV3).cast_mut().cast()) .expect("fixture async host was initialized"); - let registrations: [( - NemoRelayNativeAsyncMiddlewareKind, - &str, - NemoRelayNativeAsyncMiddlewareCb, - ); 13] = [ - ( - NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest, - "fixture_async_tool_sanitize_request", - raw_async_passthrough_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse, - "fixture_async_tool_sanitize_response", - raw_async_passthrough_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution, - "fixture_async_tool_conditional", - raw_async_allow_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept, - "fixture_async_request", - raw_async_tool_request_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept, - "fixture_async_execution", - raw_async_tool_execution_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest, - "fixture_async_llm_sanitize_request", - raw_async_passthrough_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse, - "fixture_async_llm_sanitize_response", - raw_async_passthrough_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution, - "fixture_async_llm_conditional", - raw_async_allow_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept, - "fixture_async_llm_request", - raw_async_passthrough_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept, - "fixture_async_llm_execution", - raw_async_tool_execution_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::MarkSanitize, - "fixture_async_mark", - raw_async_passthrough_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart, - "fixture_async_scope_start", - raw_async_passthrough_callback, - ), - ( - NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd, - "fixture_async_scope_end", - raw_async_passthrough_callback, - ), - ]; - for (kind, registration_name, callback) in registrations { + let registrations: [( + NemoRelayNativeAsyncMiddlewareKind, + &str, + NemoRelayNativeAsyncMiddlewareCb, + ); 13] = [ + ( + NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest, + "fixture_async_tool_sanitize_request", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse, + "fixture_async_tool_sanitize_response", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution, + "fixture_async_tool_conditional", + raw_async_allow_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept, + "fixture_async_request", + raw_async_tool_request_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept, + "fixture_async_execution", + raw_async_tool_execution_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest, + "fixture_async_llm_sanitize_request", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse, + "fixture_async_llm_sanitize_response", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution, + "fixture_async_llm_conditional", + raw_async_allow_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept, + "fixture_async_llm_request", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept, + "fixture_async_llm_execution", + raw_async_tool_execution_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::MarkSanitize, + "fixture_async_mark", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart, + "fixture_async_scope_start", + raw_async_passthrough_callback, + ), + ( + NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd, + "fixture_async_scope_end", + raw_async_passthrough_callback, + ), + ]; + for (kind, registration_name, callback) in registrations { + let status = unsafe { + ctx.register_async_middleware_raw( + kind, + registration_name, + 0, + false, + callback, + user_data, + None, + ) + }; + if status != NemoRelayStatus::Ok { + return Err(format!("async registration failed: {status:?}")); + } + } let status = unsafe { - ctx.register_async_middleware_raw( - kind, - registration_name, + ctx.register_async_stream_middleware_raw( + "fixture_async_llm_stream", 0, - false, - callback, + raw_async_stream_callback, user_data, None, ) }; if status != NemoRelayStatus::Ok { - return Err(format!("async registration failed: {status:?}")); + return Err(format!("async stream registration failed: {status:?}")); } - } - let status = unsafe { - ctx.register_async_stream_middleware_raw( - "fixture_async_llm_stream", - 0, - raw_async_stream_callback, - user_data, - None, - ) - }; - if status != NemoRelayStatus::Ok { - return Err(format!("async stream registration failed: {status:?}")); - } Ok(()) } } From 8def88e25ee76172e73bb9bda25f722a59bcc795 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 08:03:07 -0400 Subject: [PATCH 54/83] fix(runtime): preserve nested publication order Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 93 +++++++----- .../subscriber_dispatcher_tests.rs | 134 +++++++++++++++++- 2 files changed, 192 insertions(+), 35 deletions(-) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index ae1a943b3..84519e1d3 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -83,6 +83,7 @@ pub(crate) type EventTransformFn = Box< >; mod native { + use futures_util::FutureExt; use std::cell::{Cell, RefCell}; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Mutex; @@ -580,22 +581,46 @@ mod native { let previous_scope_stack = capture_thread_scope_stack(); set_thread_scope_stack(scope_stack); let _dispatch_guard = DispatchGuard::enter(); - let Some(event) = - sanitize_event_snapshot(*event, transform, sanitizers, publication_context) - else { - restore_thread_scope_stack(previous_scope_stack); - return; - }; - for subscriber in subscribers { - if catch_unwind(AssertUnwindSafe(|| subscriber(&event))).is_err() { - log::error!( - target: "nemo_relay.runtime", - event = "subscriber_callback_panicked"; - "Event subscriber callback panicked" - ); + let (event, nested_publications) = + sanitize_event_snapshot(*event, transform, sanitizers, publication_context); + if let Some(event) = event { + for subscriber in subscribers { + if catch_unwind(AssertUnwindSafe(|| subscriber(&event))).is_err() { + log::error!( + target: "nemo_relay.runtime", + event = "subscriber_callback_panicked"; + "Event subscriber callback panicked" + ); + } } } restore_thread_scope_stack(previous_scope_stack); + // Publications emitted while transforming or sanitizing this event + // are causally nested within it. Drain them before the dispatcher + // consumes messages that callers may already have queued afterward. + for publication in nested_publications { + handle_message(publication); + } + } + + fn run_with_nested_publication_buffer( + runtime: &tokio::runtime::Runtime, + publication_context: Option, + future: F, + ) -> (std::thread::Result, Vec) { + runtime.block_on(ASYNC_PUBLICATION_MESSAGES.scope( + RefCell::new(Some(Vec::new())), + async move { + let output = + AssertUnwindSafe(TASK_PUBLICATION_CONTEXT.scope(publication_context, future)) + .catch_unwind() + .await; + let publications = ASYNC_PUBLICATION_MESSAGES + .with(|messages| messages.borrow_mut().take()) + .unwrap_or_default(); + (output, publications) + }, + )) } /// Apply a transform and sanitizers on the dispatcher thread. A transform @@ -607,7 +632,7 @@ mod native { transform: Option, sanitizers: Vec>, publication_context: Option, - ) -> Option { + ) -> (Option, Vec) { let state = process_state(); let mut runtime = state .sanitizer_runtime @@ -627,20 +652,18 @@ mod native { "Event sanitizer runtime failed; dropping events: {error}" ); } - return None; + return (None, Vec::new()); } }; let transform_context = publication_context.clone(); - let transformed = match catch_unwind(AssertUnwindSafe(|| { - runtime.block_on( - TASK_PUBLICATION_CONTEXT.scope(transform_context, async move { - match transform { - Some(transform) => transform(event).await, - None => event, - } - }), - ) - })) { + let (transformed, mut nested_publications) = + run_with_nested_publication_buffer(runtime, transform_context, async move { + match transform { + Some(transform) => transform(event).await, + None => event, + } + }); + let transformed = match transformed { Ok(event) => event, Err(_) => { log::error!( @@ -648,19 +671,20 @@ mod native { event = "event_transform_panicked"; "Event transform panicked; dropping the event" ); - return None; + return (None, nested_publications); } }; if sanitizers.is_empty() { - return Some(transformed); + return (Some(transformed), nested_publications); } let fallback = transformed.clone(); - match catch_unwind(AssertUnwindSafe(|| { - runtime.block_on(TASK_PUBLICATION_CONTEXT.scope( - publication_context, - NemoRelayContextState::event_sanitize_snapshot_chain(transformed, &sanitizers), - )) - })) { + let (sanitized, sanitizer_publications) = run_with_nested_publication_buffer( + runtime, + publication_context, + NemoRelayContextState::event_sanitize_snapshot_chain(transformed, &sanitizers), + ); + nested_publications.extend(sanitizer_publications); + let event = match sanitized { Ok(event) => Some(event), Err(_) => { log::error!( @@ -670,7 +694,8 @@ mod native { ); Some(fallback) } - } + }; + (event, nested_publications) } pub(super) fn prepare_for_fork() { diff --git a/crates/core/tests/integration/subscriber_dispatcher_tests.rs b/crates/core/tests/integration/subscriber_dispatcher_tests.rs index b874d7ef6..53f702899 100644 --- a/crates/core/tests/integration/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/integration/subscriber_dispatcher_tests.rs @@ -10,7 +10,8 @@ use std::time::Duration; use nemo_relay::api::event::Event; use nemo_relay::api::registry::{ deregister_mark_sanitize_guardrail, deregister_scope_sanitize_end_guardrail, - register_mark_sanitize_guardrail, register_scope_sanitize_end_guardrail, + deregister_tool_sanitize_request_guardrail, register_mark_sanitize_guardrail, + register_scope_sanitize_end_guardrail, register_tool_sanitize_request_guardrail, }; use nemo_relay::api::runtime::{ NemoRelayContextState, create_scope_stack, current_scope_stack, global_context, @@ -20,6 +21,7 @@ use nemo_relay::api::scope::{ EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeType, event, pop_scope, push_scope, }; use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber}; +use nemo_relay::api::tool::{ToolCallEndParams, ToolCallParams, tool_call, tool_call_end}; use nemo_relay::error::FlowError; use serde_json::json; @@ -107,6 +109,136 @@ fn dispatcher_preserves_event_order() { assert_eq!(observed.lock().unwrap().as_slice(), ["one", "two"]); } +#[test] +fn nested_event_sanitizer_publication_precedes_already_queued_events() { + let _lock = TEST_MUTEX.lock().unwrap(); + flush_subscribers().unwrap(); + reset_global(); + setup_isolated_thread(); + + let observed = Arc::new(Mutex::new(Vec::new())); + let observed_events = Arc::clone(&observed); + register_subscriber( + "nested-event-order-subscriber", + Arc::new(move |event| { + observed_events + .lock() + .unwrap() + .push(event.name().to_string()); + }), + ) + .unwrap(); + + let (started_tx, started_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); + let release_rx = Arc::new(Mutex::new(release_rx)); + register_mark_sanitize_guardrail( + "nested-event-order-sanitizer", + 0, + Arc::new(move |event, fields| { + let release_rx = Arc::clone(&release_rx); + let started_tx = started_tx.clone(); + Box::pin(async move { + if event.name() == "outer-event" { + started_tx.send(()).unwrap(); + release_rx.lock().unwrap().recv().unwrap(); + emit_mark("nested-event"); + } + Ok(fields) + }) + }), + ) + .unwrap(); + + emit_mark("outer-event"); + started_rx + .recv_timeout(Duration::from_secs(1)) + .expect("outer sanitizer should start"); + emit_mark("later-event"); + release_tx.send(()).unwrap(); + flush_subscribers().unwrap(); + + assert_eq!( + observed.lock().unwrap().as_slice(), + ["outer-event", "nested-event", "later-event"] + ); + deregister_mark_sanitize_guardrail("nested-event-order-sanitizer").unwrap(); + deregister_subscriber("nested-event-order-subscriber").unwrap(); +} + +#[test] +fn nested_request_sanitizer_publication_precedes_manual_end_event() { + let _lock = TEST_MUTEX.lock().unwrap(); + flush_subscribers().unwrap(); + reset_global(); + setup_isolated_thread(); + + let observed = Arc::new(Mutex::new(Vec::new())); + let observed_events = Arc::clone(&observed); + register_subscriber( + "nested-transform-order-subscriber", + Arc::new(move |event| { + observed_events + .lock() + .unwrap() + .push(event.name().to_string()); + }), + ) + .unwrap(); + + let (started_tx, started_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); + let release_rx = Arc::new(Mutex::new(release_rx)); + register_tool_sanitize_request_guardrail( + "nested-transform-order-sanitizer", + 0, + Arc::new(move |name, args| { + let release_rx = Arc::clone(&release_rx); + let started_tx = started_tx.clone(); + Box::pin(async move { + if name == "manual-ordered-tool" { + started_tx.send(()).unwrap(); + release_rx.lock().unwrap().recv().unwrap(); + emit_mark("nested-transform-event"); + } + Ok(args) + }) + }), + ) + .unwrap(); + + let handle = tool_call( + ToolCallParams::builder() + .name("manual-ordered-tool") + .args(json!({"input": true})) + .build(), + ) + .unwrap(); + started_rx + .recv_timeout(Duration::from_secs(1)) + .expect("request sanitizer should start"); + tool_call_end( + ToolCallEndParams::builder() + .handle(&handle) + .result(json!({"output": true})) + .build(), + ) + .unwrap(); + release_tx.send(()).unwrap(); + flush_subscribers().unwrap(); + + assert_eq!( + observed.lock().unwrap().as_slice(), + [ + "manual-ordered-tool", + "nested-transform-event", + "manual-ordered-tool" + ] + ); + deregister_tool_sanitize_request_guardrail("nested-transform-order-sanitizer").unwrap(); + deregister_subscriber("nested-transform-order-subscriber").unwrap(); +} + #[test] fn queued_sanitizer_keeps_the_emission_time_scope_after_pop() { let _lock = TEST_MUTEX.lock().unwrap(); From 03b4c8decc4d4f18ac6df27d6664305e1ea2a8cb Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 09:04:34 -0400 Subject: [PATCH 55/83] fix(runtime): preserve foreign nested publication order Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 397 ++++++------- .../tests/unit/subscriber_dispatcher_tests.rs | 276 ++++++++++ crates/node/src/api/mod.rs | 520 ++++++++++-------- crates/node/src/callback_factory.rs | 28 +- crates/node/src/promise_call.rs | 68 ++- crates/node/src/types/mod.rs | 8 +- crates/node/tests/event_sanitizers_tests.mjs | 39 ++ crates/python/src/py_api/mod.rs | 211 ++++--- crates/python/src/py_callable.rs | 74 ++- crates/python/src/py_types/core.rs | 11 +- .../tests/coverage/py_types_coverage_tests.rs | 5 +- python/nemo_relay/__init__.py | 3 + python/tests/test_event_sanitizers.py | 30 + 13 files changed, 1100 insertions(+), 570 deletions(-) create mode 100644 crates/core/tests/unit/subscriber_dispatcher_tests.rs diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index 84519e1d3..f5568c3a1 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -83,7 +83,6 @@ pub(crate) type EventTransformFn = Box< >; mod native { - use futures_util::FutureExt; use std::cell::{Cell, RefCell}; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Mutex; @@ -99,7 +98,7 @@ mod native { }; use crate::error::FlowError; - enum DispatcherMessage { + pub(super) enum DispatcherMessage { Deliver { event: Box, transform: Option, @@ -123,6 +122,53 @@ mod native { std::result::Result, String>, >; + /// Opaque routing handle for publications emitted on foreign callback threads. + #[derive(Clone)] + pub struct PublicationBuffer { + messages: Arc>>>, + } + + impl PublicationBuffer { + fn new(messages: Option>) -> Self { + Self { + messages: Arc::new(Mutex::new(messages)), + } + } + + fn enabled() -> Self { + Self::new(Some(Vec::new())) + } + + fn push(&self, message: DispatcherMessage) -> std::result::Result<(), DispatcherMessage> { + let mut messages = self + .messages + .lock() + .unwrap_or_else(|error| error.into_inner()); + match messages.as_mut() { + Some(messages) => { + messages.push(message); + Ok(()) + } + None => Err(message), + } + } + + fn take(&self) -> Vec { + self.messages + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + .unwrap_or_default() + } + + fn is_active(&self) -> bool { + self.messages + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_some() + } + } + struct ProcessState { dispatcher: Mutex, sanitizer_runtime: Mutex, @@ -152,15 +198,17 @@ mod native { thread_local! { static IN_DISPATCHER: Cell = const { Cell::new(false) }; static PREPARED_FORK_STATE: Cell<*mut ProcessState> = const { Cell::new(std::ptr::null_mut()) }; + static THREAD_PUBLICATION_BUFFER: RefCell> = const { RefCell::new(None) }; } tokio::task_local! { - static ASYNC_PUBLICATION_MESSAGES: RefCell>>; + static ASYNC_PUBLICATION_BUFFER: PublicationBuffer; } struct DispatchGuard; + struct ThreadPublicationBufferGuard(Option); pub(crate) struct AsyncPublication { - sender: Sender>, + pub(super) sender: Sender>, } fn process_state() -> &'static ProcessState { @@ -196,6 +244,14 @@ mod native { } } + impl Drop for ThreadPublicationBufferGuard { + fn drop(&mut self) { + THREAD_PUBLICATION_BUFFER.with(|current| { + current.replace(self.0.take()); + }); + } + } + fn immutable_scope_stack(scope_stack: &ScopeStackHandle) -> Option { match snapshot_scope_stack(scope_stack) { Ok(scope_stack) => Some(scope_stack), @@ -382,16 +438,13 @@ mod native { /// A later flush waits for the task and drains its buffered publications /// at the reserved position before acknowledging the flush. pub(super) fn register_async_publication() -> Option { - let sender = dispatcher_sender().ok()?; let (publication_tx, publication_rx) = mpsc::channel(); - sender - .send(DispatcherMessage::Barrier { - publications: publication_rx, - }) - .ok() - .map(|_| AsyncPublication { - sender: publication_tx, - }) + enqueue_dispatch_message(DispatcherMessage::Barrier { + publications: publication_rx, + }) + .then_some(AsyncPublication { + sender: publication_tx, + }) } pub(super) fn flush_subscribers() -> Result<()> { @@ -424,35 +477,70 @@ mod native { } pub(super) fn in_dispatcher_callback() -> bool { - IN_DISPATCHER.with(Cell::get) || ASYNC_PUBLICATION_MESSAGES.try_with(|_| ()).is_ok() + IN_DISPATCHER.with(Cell::get) + || ASYNC_PUBLICATION_BUFFER.try_with(|_| ()).is_ok() + || THREAD_PUBLICATION_BUFFER.with(|buffer| { + buffer + .borrow() + .as_ref() + .is_some_and(PublicationBuffer::is_active) + }) + } + + pub(super) fn capture_nested_publication_buffer() -> Option { + ASYNC_PUBLICATION_BUFFER + .try_with(Clone::clone) + .ok() + .filter(PublicationBuffer::is_active) + .or_else(|| { + THREAD_PUBLICATION_BUFFER + .with(|buffer| buffer.borrow().clone()) + .filter(PublicationBuffer::is_active) + }) + } + + pub(super) fn with_nested_publication_buffer( + buffer: Option, + f: impl FnOnce() -> T, + ) -> T { + let previous = THREAD_PUBLICATION_BUFFER.with(|current| current.replace(buffer)); + let _guard = ThreadPublicationBufferGuard(previous); + f() + } + + pub(super) fn sync_thread_publication_buffer(buffer: Option) { + THREAD_PUBLICATION_BUFFER.with(|current| { + current.replace(buffer); + }); + } + + pub(super) async fn with_task_nested_publication_buffer( + buffer: Option, + future: F, + ) -> F::Output { + match buffer { + Some(buffer) => ASYNC_PUBLICATION_BUFFER.scope(buffer, future).await, + None => future.await, + } } pub(super) async fn with_async_publication_context( publication: Option, future: F, ) -> F::Output { - if ASYNC_PUBLICATION_MESSAGES.try_with(|_| ()).is_ok() { + if ASYNC_PUBLICATION_BUFFER.try_with(|_| ()).is_ok() { future.await } else { - let (output, publications) = ASYNC_PUBLICATION_MESSAGES - .scope( - RefCell::new(publication.as_ref().map(|_| Vec::new())), - async { - let output = future.await; - let publications = ASYNC_PUBLICATION_MESSAGES - .with(|messages| messages.borrow_mut().take()); - (output, publications) - }, - ) - .await; - if let (Some(publication), Some(publications)) = (publication, publications) { - let _ = publication.sender.send(publications); + let buffer = PublicationBuffer::new(publication.as_ref().map(|_| Vec::new())); + let output = ASYNC_PUBLICATION_BUFFER.scope(buffer.clone(), future).await; + if let Some(publication) = publication { + let _ = publication.sender.send(buffer.take()); } output } } - fn dispatcher_sender() -> std::result::Result, String> { + pub(super) fn dispatcher_sender() -> std::result::Result, String> { let mut dispatcher = process_state() .dispatcher .lock() @@ -488,21 +576,26 @@ mod native { } } - fn enqueue_dispatch_message(message: DispatcherMessage) -> bool { - let mut message = Some(message); - let buffered = ASYNC_PUBLICATION_MESSAGES - .try_with(|messages| { - let mut messages = messages.borrow_mut(); - match messages.as_mut() { - Some(messages) => { - messages.push(message.take().expect("message is buffered once")); - true - } - None => false, - } - }) - .unwrap_or(false); - buffered || send_dispatch_message(message.expect("unbuffered message remains available")) + pub(super) fn enqueue_dispatch_message(message: DispatcherMessage) -> bool { + let message = if let Ok(buffer) = ASYNC_PUBLICATION_BUFFER.try_with(Clone::clone) { + match buffer.push(message) { + Ok(()) => return true, + Err(message) => message, + } + } else { + message + }; + let message = if let Some(buffer) = + THREAD_PUBLICATION_BUFFER.with(|buffer| buffer.borrow().clone()) + { + match buffer.push(message) { + Ok(()) => return true, + Err(message) => message, + } + } else { + message + }; + send_dispatch_message(message) } fn start_dispatcher() -> std::result::Result, String> { @@ -608,19 +701,14 @@ mod native { publication_context: Option, future: F, ) -> (std::thread::Result, Vec) { - runtime.block_on(ASYNC_PUBLICATION_MESSAGES.scope( - RefCell::new(Some(Vec::new())), - async move { - let output = - AssertUnwindSafe(TASK_PUBLICATION_CONTEXT.scope(publication_context, future)) - .catch_unwind() - .await; - let publications = ASYNC_PUBLICATION_MESSAGES - .with(|messages| messages.borrow_mut().take()) - .unwrap_or_default(); - (output, publications) - }, - )) + let buffer = PublicationBuffer::enabled(); + let output = catch_unwind(AssertUnwindSafe(|| { + runtime.block_on(ASYNC_PUBLICATION_BUFFER.scope( + buffer.clone(), + TASK_PUBLICATION_CONTEXT.scope(publication_context, future), + )) + })); + (output, buffer.take()) } /// Apply a transform and sanitizers on the dispatcher thread. A transform @@ -732,171 +820,46 @@ mod native { PROCESS_STATE.store(state, Ordering::Release); }); } +} - #[cfg(test)] - mod tests { - use super::*; +#[cfg(test)] +#[path = "../../../tests/unit/subscriber_dispatcher_tests.rs"] +mod tests; - #[test] - fn flush_waits_for_active_but_not_later_publication_barriers() { - let _lock = crate::shared_runtime::runtime_owner_test_mutex() - .lock() - .unwrap_or_else(|error| error.into_inner()); - flush_subscribers().unwrap(); - let first = register_async_publication().expect("first publication barrier"); - let sender = dispatcher_sender().expect("dispatcher sender"); - let delivered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let subscriber: EventSubscriberFn = { - let delivered = delivered.clone(); - std::sync::Arc::new(move |event| { - delivered - .lock() - .unwrap_or_else(|error| error.into_inner()) - .push(event.name().to_string()); - }) - }; - let queued_event = serde_json::from_value(serde_json::json!({ - "kind": "mark", - "atof_version": "0.1", - "uuid": "019c1df6-4a57-7000-8000-000000000001", - "timestamp": "2026-07-28T00:00:00Z", - "name": "queued-before-flush" - })) - .expect("valid event"); - sender - .send(DispatcherMessage::Deliver { - event: Box::new(queued_event), - transform: None, - sanitizers: Vec::new(), - subscribers: vec![subscriber.clone()], - scope_stack: current_scope_stack(), - publication_context: None, - }) - .unwrap(); - let (flush_tx, flush_rx) = mpsc::channel(); - sender - .send(DispatcherMessage::Flush { done: flush_tx }) - .unwrap(); - let later = register_async_publication().expect("later publication barrier"); +#[doc(hidden)] +pub use native::PublicationBuffer; - assert!( - flush_rx - .recv_timeout(std::time::Duration::from_millis(50)) - .is_err(), - "flush must wait for an active publication barrier" - ); - let deferred_event = serde_json::from_value(serde_json::json!({ - "kind": "mark", - "atof_version": "0.1", - "uuid": "019c1df6-4a57-7000-8000-000000000002", - "timestamp": "2026-07-28T00:00:00Z", - "name": "deferred-at-barrier" - })) - .expect("valid event"); - first - .sender - .send(vec![DispatcherMessage::Deliver { - event: Box::new(deferred_event), - transform: None, - sanitizers: Vec::new(), - subscribers: vec![subscriber], - scope_stack: current_scope_stack(), - publication_context: None, - }]) - .unwrap(); - flush_rx - .recv_timeout(std::time::Duration::from_secs(1)) - .expect("flush queued before the later barrier must complete"); - assert_eq!( - *delivered.lock().unwrap_or_else(|error| error.into_inner()), - ["deferred-at-barrier", "queued-before-flush"], - "the barrier must publish deferred work at its reserved FIFO position" - ); - later.sender.send(Vec::new()).unwrap(); - flush_subscribers().unwrap(); - } +/// Capture the active nested-publication buffer for a foreign callback thread. +#[doc(hidden)] +pub fn capture_nested_publication_buffer() -> Option { + native::capture_nested_publication_buffer() +} - #[test] - fn flush_does_not_wait_for_later_delivery() { - let _lock = crate::shared_runtime::runtime_owner_test_mutex() - .lock() - .unwrap_or_else(|error| error.into_inner()); - flush_subscribers().unwrap(); - let barrier = register_async_publication().expect("publication barrier"); - let sender = dispatcher_sender().expect("dispatcher sender"); - let (flush_tx, flush_rx) = mpsc::channel(); - sender - .send(DispatcherMessage::Flush { done: flush_tx }) - .unwrap(); - - let (release_tx, release_rx) = tokio::sync::oneshot::channel(); - let event = serde_json::from_value(serde_json::json!({ - "kind": "mark", - "atof_version": "0.1", - "uuid": "019c1df6-4a57-7000-8000-000000000003", - "timestamp": "2026-07-28T00:00:00Z", - "name": "queued-after-flush" - })) - .expect("valid event"); - sender - .send(DispatcherMessage::Deliver { - event: Box::new(event), - transform: Some(Box::new(move |event| { - Box::pin(async move { - let _ = release_rx.await; - event - }) - })), - sanitizers: Vec::new(), - subscribers: Vec::new(), - scope_stack: current_scope_stack(), - publication_context: None, - }) - .unwrap(); - barrier.sender.send(Vec::new()).unwrap(); - - let flush_result = flush_rx.recv_timeout(std::time::Duration::from_millis(100)); - let _ = release_tx.send(()); - flush_subscribers().unwrap(); - assert!( - flush_result.is_ok(), - "a delivery queued after a flush must not delay that flush" - ); - } +/// Route synchronous publications on a foreign callback thread into the +/// dispatcher invocation that scheduled the callback. +#[doc(hidden)] +pub fn with_nested_publication_buffer( + buffer: Option, + f: impl FnOnce() -> T, +) -> T { + native::with_nested_publication_buffer(buffer, f) +} - #[test] - fn detached_publications_share_one_background_executor_thread() { - let _lock = crate::shared_runtime::runtime_owner_test_mutex() - .lock() - .unwrap_or_else(|error| error.into_inner()); - let (started_tx, started_rx) = mpsc::channel(); - let (release_tx, release_rx) = tokio::sync::watch::channel(false); - for _ in 0..32 { - let started_tx = started_tx.clone(); - let mut release_rx = release_rx.clone(); - assert!(spawn_background_publication(async move { - started_tx.send(std::thread::current().id()).unwrap(); - while !*release_rx.borrow() { - release_rx.changed().await.unwrap(); - } - })); - } - drop(started_tx); - let threads = (0..32) - .map(|_| { - started_rx - .recv_timeout(std::time::Duration::from_secs(2)) - .expect("background publication should start") - }) - .collect::>(); - assert_eq!( - threads.len(), - 1, - "detached publications must not allocate one OS thread per future" - ); - release_tx.send(true).unwrap(); - } - } +/// Route publications from a foreign async callback task into the dispatcher +/// invocation that scheduled the callback. +#[doc(hidden)] +pub async fn with_task_nested_publication_buffer( + buffer: Option, + future: F, +) -> F::Output { + native::with_task_nested_publication_buffer(buffer, future).await +} + +/// Synchronize a foreign runtime's current callback publication buffer into +/// Relay's thread-local fallback. +#[doc(hidden)] +pub fn sync_thread_publication_buffer(buffer: Option) { + native::sync_thread_publication_buffer(buffer); } #[cfg(test)] diff --git a/crates/core/tests/unit/subscriber_dispatcher_tests.rs b/crates/core/tests/unit/subscriber_dispatcher_tests.rs new file mode 100644 index 000000000..946395ee7 --- /dev/null +++ b/crates/core/tests/unit/subscriber_dispatcher_tests.rs @@ -0,0 +1,276 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +use super::EventSubscriberFn; +use super::native::{ + DispatcherMessage, dispatcher_sender, enqueue_dispatch_message, flush_subscribers, + register_async_publication, spawn_background_publication, +}; +use crate::api::runtime::scope_stack::current_scope_stack; +use std::sync::{Arc, Mutex, mpsc}; + +#[test] +fn flush_waits_for_active_but_not_later_publication_barriers() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + flush_subscribers().unwrap(); + let first = register_async_publication().expect("first publication barrier"); + let sender = dispatcher_sender().expect("dispatcher sender"); + let delivered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let subscriber: EventSubscriberFn = { + let delivered = delivered.clone(); + std::sync::Arc::new(move |event| { + delivered + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(event.name().to_string()); + }) + }; + let queued_event = serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000001", + "timestamp": "2026-07-28T00:00:00Z", + "name": "queued-before-flush" + })) + .expect("valid event"); + sender + .send(DispatcherMessage::Deliver { + event: Box::new(queued_event), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![subscriber.clone()], + scope_stack: current_scope_stack(), + publication_context: None, + }) + .unwrap(); + let (flush_tx, flush_rx) = mpsc::channel(); + sender + .send(DispatcherMessage::Flush { done: flush_tx }) + .unwrap(); + let later = register_async_publication().expect("later publication barrier"); + + assert!( + flush_rx + .recv_timeout(std::time::Duration::from_millis(50)) + .is_err(), + "flush must wait for an active publication barrier" + ); + let deferred_event = serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000002", + "timestamp": "2026-07-28T00:00:00Z", + "name": "deferred-at-barrier" + })) + .expect("valid event"); + first + .sender + .send(vec![DispatcherMessage::Deliver { + event: Box::new(deferred_event), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![subscriber], + scope_stack: current_scope_stack(), + publication_context: None, + }]) + .unwrap(); + flush_rx + .recv_timeout(std::time::Duration::from_secs(1)) + .expect("flush queued before the later barrier must complete"); + assert_eq!( + *delivered.lock().unwrap_or_else(|error| error.into_inner()), + ["deferred-at-barrier", "queued-before-flush"], + "the barrier must publish deferred work at its reserved FIFO position" + ); + later.sender.send(Vec::new()).unwrap(); + flush_subscribers().unwrap(); +} + +#[test] +fn flush_does_not_wait_for_later_delivery() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + flush_subscribers().unwrap(); + let barrier = register_async_publication().expect("publication barrier"); + let sender = dispatcher_sender().expect("dispatcher sender"); + let (flush_tx, flush_rx) = mpsc::channel(); + sender + .send(DispatcherMessage::Flush { done: flush_tx }) + .unwrap(); + + let (release_tx, release_rx) = tokio::sync::oneshot::channel(); + let event = serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000003", + "timestamp": "2026-07-28T00:00:00Z", + "name": "queued-after-flush" + })) + .expect("valid event"); + sender + .send(DispatcherMessage::Deliver { + event: Box::new(event), + transform: Some(Box::new(move |event| { + Box::pin(async move { + let _ = release_rx.await; + event + }) + })), + sanitizers: Vec::new(), + subscribers: Vec::new(), + scope_stack: current_scope_stack(), + publication_context: None, + }) + .unwrap(); + barrier.sender.send(Vec::new()).unwrap(); + + let flush_result = flush_rx.recv_timeout(std::time::Duration::from_millis(100)); + let _ = release_tx.send(()); + flush_subscribers().unwrap(); + assert!( + flush_result.is_ok(), + "a delivery queued after a flush must not delay that flush" + ); +} + +#[test] +fn nested_publication_barrier_precedes_already_queued_delivery() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + flush_subscribers().unwrap(); + let sender = dispatcher_sender().expect("dispatcher sender"); + let delivered = Arc::new(Mutex::new(Vec::new())); + let subscriber: EventSubscriberFn = { + let delivered = Arc::clone(&delivered); + Arc::new(move |event| { + delivered + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(event.name().to_string()); + }) + }; + let event = |uuid: &str, name: &str| { + serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": uuid, + "timestamp": "2026-07-28T00:00:00Z", + "name": name + })) + .expect("valid event") + }; + let (started_tx, started_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); + let nested_subscriber = subscriber.clone(); + let nested_scope_stack = current_scope_stack(); + sender + .send(DispatcherMessage::Deliver { + event: Box::new(event("019c1df6-4a57-7000-8000-000000000004", "outer")), + transform: Some(Box::new(move |event| { + Box::pin(async move { + started_tx.send(()).unwrap(); + release_rx.recv().unwrap(); + assert!(enqueue_dispatch_message(DispatcherMessage::Deliver { + event: Box::new( + serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000005", + "timestamp": "2026-07-28T00:00:00Z", + "name": "nested-start" + })) + .expect("valid event"), + ), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![nested_subscriber.clone()], + scope_stack: nested_scope_stack.clone(), + publication_context: None, + })); + let publication = + register_async_publication().expect("nested publication barrier"); + publication + .sender + .send(vec![DispatcherMessage::Deliver { + event: Box::new( + serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000006", + "timestamp": "2026-07-28T00:00:00Z", + "name": "nested-end" + })) + .expect("valid event"), + ), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![nested_subscriber], + scope_stack: nested_scope_stack, + publication_context: None, + }]) + .unwrap(); + event + }) + })), + sanitizers: Vec::new(), + subscribers: vec![subscriber.clone()], + scope_stack: current_scope_stack(), + publication_context: None, + }) + .unwrap(); + started_rx + .recv_timeout(std::time::Duration::from_secs(1)) + .expect("outer transform should start"); + sender + .send(DispatcherMessage::Deliver { + event: Box::new(event("019c1df6-4a57-7000-8000-000000000007", "later")), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![subscriber], + scope_stack: current_scope_stack(), + publication_context: None, + }) + .unwrap(); + release_tx.send(()).unwrap(); + flush_subscribers().unwrap(); + assert_eq!( + *delivered.lock().unwrap_or_else(|error| error.into_inner()), + ["outer", "nested-start", "nested-end", "later"] + ); +} + +#[test] +fn detached_publications_share_one_background_executor_thread() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + let (started_tx, started_rx) = mpsc::channel(); + let (release_tx, release_rx) = tokio::sync::watch::channel(false); + for _ in 0..32 { + let started_tx = started_tx.clone(); + let mut release_rx = release_rx.clone(); + assert!(spawn_background_publication(async move { + started_tx.send(std::thread::current().id()).unwrap(); + while !*release_rx.borrow() { + release_rx.changed().await.unwrap(); + } + })); + } + drop(started_tx); + let threads = (0..32) + .map(|_| { + started_rx + .recv_timeout(std::time::Duration::from_secs(2)) + .expect("background publication should start") + }) + .collect::>(); + assert_eq!( + threads.len(), + 1, + "detached publications must not allocate one OS thread per future" + ); + release_tx.send(true).unwrap(); +} diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 7d25e7170..c6bb6cf86 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -31,6 +31,9 @@ use tokio_stream::{Stream, StreamExt}; use nemo_relay::api::llm as core_llm_api; use nemo_relay::api::llm::{LlmAttributes, LlmRequest}; use nemo_relay::api::registry as core_registry_api; +use nemo_relay::api::runtime::subscriber_dispatcher::{ + PublicationBuffer, with_nested_publication_buffer, +}; use nemo_relay::api::runtime::{ EventSanitizeFn, LlmExecutionNextFn, LlmJsonStream, LlmStreamExecutionNextFn, LlmStreamInner, ToolExecutionNextFn, @@ -85,13 +88,25 @@ use crate::promise_call::with_publication_callback_context; use crate::stream::LlmStream; use crate::types::{LlmHandle, ScopeHandle, ScopeStack, ScopeType, ToolHandle}; +fn effective_scope_context( + env: &Env, +) -> napi::Result<( + nemo_relay::api::runtime::ScopeStackHandle, + Option, +)> { + Ok(callback_factory::callback_scope_stack(env)? + .unwrap_or_else(|| (current_scope_stack_handle(), None))) +} + fn effective_scope_stack(env: &Env) -> napi::Result { - Ok(callback_factory::callback_scope_stack(env)?.unwrap_or_else(current_scope_stack_handle)) + effective_scope_context(env).map(|(scope_stack, _)| scope_stack) } fn with_effective_scope_stack(env: &Env, callback: impl FnOnce() -> T) -> napi::Result { - let scope_stack = effective_scope_stack(env)?; - Ok(with_scope_stack_handle(scope_stack, callback)) + let (scope_stack, publication_buffer) = effective_scope_context(env)?; + Ok(with_scope_stack_handle(scope_stack, || { + with_nested_publication_buffer(publication_buffer, callback) + })) } fn effective_scope_top( @@ -1588,6 +1603,7 @@ fn propagation_context_to_napi( pub fn create_scope_stack() -> ScopeStack { ScopeStack { inner: create_scope_stack_handle(), + publication_buffer: None, } } @@ -1657,16 +1673,22 @@ pub fn with_scope_stack( return Ok(value); } with_scope_stack_handle(stack.inner.clone(), || { - callback.call::(None, &[]) + with_nested_publication_buffer(stack.publication_buffer.clone(), || { + callback.call::(None, &[]) + }) }) } /// Returns the current execution context's scope stack handle. #[napi] pub fn current_scope_stack(env: Env) -> napi::Result { - Ok(ScopeStack { - inner: effective_scope_stack(&env)?, - }) + if let Some((inner, publication_buffer)) = callback_factory::callback_scope_stack(&env)? { + return Ok(ScopeStack { + inner, + publication_buffer, + }); + } + Ok(ScopeStack::from(current_scope_stack_handle())) } /// Binds a scope stack to the current thread. @@ -1917,19 +1939,21 @@ pub fn with_scope( ) -> Result { let attrs = ScopeAttributes::from_bits_truncate(attributes.unwrap_or(0)); let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; let scope_handle = with_scope_stack_handle(scope_stack.clone(), || { - core_scope_api::push_scope( - core_scope_api::PushScopeParams::builder() - .name(name.as_str()) - .scope_type(scope_type.into()) - .parent_opt(handle.map(|h| &h.inner)) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .input_opt(opt_json(input)) - .build(), - ) + with_nested_publication_buffer(publication_buffer.clone(), || { + core_scope_api::push_scope( + core_scope_api::PushScopeParams::builder() + .name(name.as_str()) + .scope_type(scope_type.into()) + .parent_opt(handle.map(|h| &h.inner)) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .input_opt(opt_json(input)) + .build(), + ) + }) }) .map(ScopeHandle::from) .map_err(to_napi_err)?; @@ -1943,19 +1967,22 @@ pub fn with_scope( let callback_handle = scope_handle.inner.clone(); // Create a promise-aware wrapper so we handle both sync and async callbacks. + let error_publication_buffer = publication_buffer.clone(); let pa_fn = std::sync::Arc::new( crate::promise_call::PromiseAwareFn::new(&env, &callback).map_err(|e| { let status_message = format!("failed to create PromiseAwareFn: {e}"); let _ = with_scope_stack_handle(scope_stack.clone(), || { - core_scope_api::pop_scope( - core_scope_api::PopScopeParams::builder() - .handle_uuid(&scope_uuid) - .metadata_opt(Some(otel_status_metadata( - "ERROR", - Some(status_message.clone()), - ))) - .build(), - ) + with_nested_publication_buffer(error_publication_buffer.clone(), || { + core_scope_api::pop_scope( + core_scope_api::PopScopeParams::builder() + .handle_uuid(&scope_uuid) + .metadata_opt(Some(otel_status_metadata( + "ERROR", + Some(status_message.clone()), + ))) + .build(), + ) + }) }); napi::Error::from_reason(status_message) })?, @@ -1963,36 +1990,42 @@ pub fn with_scope( env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let build_handle: crate::promise_call::Arg0Builder = - Box::new(move |env: &Env| { - let raw = unsafe { - ::to_napi_value( - env.raw(), - ScopeHandle::from(callback_handle), - )? - }; - Ok(unsafe { JsUnknown::from_raw_unchecked(env.raw(), raw) }) - }); - - let result = pa_fn.call_with_arg0(build_handle).await; - let metadata = match &result { - Ok(_) => otel_status_metadata("OK", None), - Err(error) => otel_status_metadata("ERROR", Some(error.to_string())), - }; - // Always pop the scope, even on error. - let _ = core_scope_api::pop_scope( - core_scope_api::PopScopeParams::builder() - .handle_uuid(&scope_uuid) - .metadata_opt(Some(metadata)) - .build(), - ); - result.map_err(to_napi_err) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let build_handle: crate::promise_call::Arg0Builder = + Box::new(move |env: &Env| { + let raw = unsafe { + ::to_napi_value( + env.raw(), + ScopeHandle::from(callback_handle), + )? + }; + Ok(unsafe { JsUnknown::from_raw_unchecked(env.raw(), raw) }) + }); + + let result = pa_fn.call_with_arg0(build_handle).await; + let metadata = match &result { + Ok(_) => otel_status_metadata("OK", None), + Err(error) => { + otel_status_metadata("ERROR", Some(error.to_string())) + } + }; + // Always pop the scope, even on error. + let _ = core_scope_api::pop_scope( + core_scope_api::PopScopeParams::builder() + .handle_uuid(&scope_uuid) + .metadata_opt(Some(metadata)) + .build(), + ); + result.map_err(to_napi_err) + }) + .await + }, + ) .await }, |_env, result| Ok(result), @@ -2131,7 +2164,7 @@ pub fn tool_call_execute( ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; let parent = handle .map(|h| h.inner.clone()) .unwrap_or_else(|| effective_scope_top(&scope_stack)); @@ -2141,25 +2174,29 @@ pub fn tool_call_execute( env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_call_execute( - core_tool_api::ToolCallExecuteParams::builder() - .name(name) - .args(args) - .func(default_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .build(), - ) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_call_execute( + core_tool_api::ToolCallExecuteParams::builder() + .name(name) + .args(args) + .func(default_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .build(), + ) + .await + .map_err(to_napi_err) + }) .await - .map_err(to_napi_err) - }) - .await - }) + }, + ) .await }, |_env, result| Ok(result), @@ -2188,7 +2225,7 @@ pub fn tool_call_execute_async( ) -> Result { let attrs = ToolAttributes::from_bits_truncate(attributes.unwrap_or(0)); let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; let parent = handle .map(|h| h.inner.clone()) .unwrap_or_else(|| effective_scope_top(&scope_stack)); @@ -2207,25 +2244,29 @@ pub fn tool_call_execute_async( env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_call_execute( - core_tool_api::ToolCallExecuteParams::builder() - .name(name) - .args(args) - .func(exec_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .build(), - ) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_call_execute( + core_tool_api::ToolCallExecuteParams::builder() + .name(name) + .args(args) + .func(exec_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .build(), + ) + .await + .map_err(to_napi_err) + }) .await - .map_err(to_napi_err) - }) - .await - }) + }, + ) .await }, |_env, result| Ok(result), @@ -2340,7 +2381,7 @@ pub fn llm_call_execute( ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; let parent = handle .map(|h| h.inner.clone()) .unwrap_or_else(|| effective_scope_top(&scope_stack)); @@ -2373,27 +2414,31 @@ pub fn llm_call_execute( }); env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let params = core_llm_api::LlmCallExecuteParams::builder() - .name(name) - .request(llm_request) - .func(default_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .model_name_opt(model_name) - .codec_opt(codec) - .response_codec_opt(response_codec) - .build(); - core_llm_api::llm_call_execute(params) - .await - .map_err(to_napi_err) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let params = core_llm_api::LlmCallExecuteParams::builder() + .name(name) + .request(llm_request) + .func(default_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .model_name_opt(model_name) + .codec_opt(codec) + .response_codec_opt(response_codec) + .build(); + core_llm_api::llm_call_execute(params) + .await + .map_err(to_napi_err) + }) + .await + }, + ) .await }, move |_env, result| { @@ -2425,7 +2470,7 @@ pub fn llm_call_execute_async( ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; let parent = handle .map(|h| h.inner.clone()) .unwrap_or_else(|| effective_scope_top(&scope_stack)); @@ -2468,27 +2513,31 @@ pub fn llm_call_execute_async( env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let params = core_llm_api::LlmCallExecuteParams::builder() - .name(name) - .request(llm_request) - .func(exec_fn) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .model_name_opt(model_name) - .codec_opt(codec) - .response_codec_opt(response_codec) - .build(); - core_llm_api::llm_call_execute(params) - .await - .map_err(to_napi_err) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let params = core_llm_api::LlmCallExecuteParams::builder() + .name(name) + .request(llm_request) + .func(exec_fn) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .model_name_opt(model_name) + .codec_opt(codec) + .response_codec_opt(response_codec) + .build(); + core_llm_api::llm_call_execute(params) + .await + .map_err(to_napi_err) + }) + .await + }, + ) .await }, move |_env, result| { @@ -2535,7 +2584,7 @@ pub fn llm_stream_call_execute( ) -> Result { let attrs = LlmAttributes::from_bits_truncate(attributes.unwrap_or(0)); let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; let parent = handle .map(|h| h.inner.clone()) .unwrap_or_else(|| effective_scope_top(&scope_stack)); @@ -2611,44 +2660,49 @@ pub fn llm_stream_call_execute( let completion_codec_references = codec_references.clone(); env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id.clone(), async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - let params = core_llm_api::LlmStreamCallExecuteParams::builder() - .name(name) - .request(llm_request) - .func(default_fn) - .collector(wrapped_collector) - .finalizer(wrapped_finalizer) - .parent(parent) - .attributes(attrs) - .data_opt(opt_json(data)) - .metadata_opt(opt_json(metadata)) - .model_name_opt(model_name) - .codec_opt(codec) - .response_codec_opt(response_codec) - .build(); - let rust_stream = core_llm_api::llm_stream_call_execute(params) - .await - .map_err(to_napi_err)?; - - let (tx, rx) = tokio::sync::mpsc::channel(32); - let (cancel, cancel_rx) = tokio::sync::watch::channel(false); - let (closed, closed_rx) = tokio::sync::watch::channel(None); - tokio::spawn(with_publication_callback_context( - publication_context_id, - forward_stream_to_channel(rust_stream, tx, cancel_rx, closed), - )); - - Ok(LlmStream { - receiver: tokio::sync::Mutex::new(rx), - cancel, - closed: closed_rx, - codec_references, + with_publication_callback_context( + publication_context_id.clone(), + publication_buffer.clone(), + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + let params = core_llm_api::LlmStreamCallExecuteParams::builder() + .name(name) + .request(llm_request) + .func(default_fn) + .collector(wrapped_collector) + .finalizer(wrapped_finalizer) + .parent(parent) + .attributes(attrs) + .data_opt(opt_json(data)) + .metadata_opt(opt_json(metadata)) + .model_name_opt(model_name) + .codec_opt(codec) + .response_codec_opt(response_codec) + .build(); + let rust_stream = core_llm_api::llm_stream_call_execute(params) + .await + .map_err(to_napi_err)?; + + let (tx, rx) = tokio::sync::mpsc::channel(32); + let (cancel, cancel_rx) = tokio::sync::watch::channel(false); + let (closed, closed_rx) = tokio::sync::watch::channel(None); + tokio::spawn(with_publication_callback_context( + publication_context_id, + publication_buffer, + forward_stream_to_channel(rust_stream, tx, cancel_rx, closed), + )); + + Ok(LlmStream { + receiver: tokio::sync::Mutex::new(rx), + cancel, + closed: closed_rx, + codec_references, + }) }) - }) - .await - }) + .await + }, + ) .await }, move |_env, result| { @@ -3800,18 +3854,22 @@ pub fn scope_deregister_subscriber(scope_uuid: String, name: String) -> Result Result { let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_request_intercepts(&name, args) - .await - .map_err(to_napi_err) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_request_intercepts(&name, args) + .await + .map_err(to_napi_err) + }) + .await + }, + ) .await }, |_env, result| Ok(result), @@ -3823,18 +3881,22 @@ pub fn tool_request_intercepts(env: Env, name: String, args: Json) -> Result Result { let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_tool_api::tool_conditional_execution(&name, &args) - .await - .map_err(to_napi_err) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_tool_api::tool_conditional_execution(&name, &args) + .await + .map_err(to_napi_err) + }) + .await + }, + ) .await }, |env, _| env.get_undefined(), @@ -3851,26 +3913,30 @@ pub fn llm_request_intercepts(env: Env, name: String, request: Json) -> Result Result { let llm_request: LlmRequest = serde_json::from_value(request) .map_err(|e| napi::Error::from_reason(format!("invalid LlmRequest: {e}")))?; let publication_context_id = callback_factory::event_sanitizer_callback_context_id(&env)?; - let scope_stack = effective_scope_stack(&env)?; + let (scope_stack, publication_buffer) = effective_scope_context(&env)?; env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - core_llm_api::llm_conditional_execution(&llm_request) - .await - .map_err(to_napi_err) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + core_llm_api::llm_conditional_execution(&llm_request) + .await + .map_err(to_napi_err) + }) + .await + }, + ) .await }, |env, _| env.get_undefined(), diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index a404ce60e..b44d76a7d 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -6,6 +6,7 @@ use napi::bindgen_prelude::FromNapiValue; use napi::{Env, JsFunction, JsObject, JsUnknown, NapiRaw, NapiValue, ValueType}; use nemo_relay::api::runtime::ScopeStackHandle; +use nemo_relay::api::runtime::subscriber_dispatcher::PublicationBuffer; use crate::types::ScopeStack; @@ -290,7 +291,9 @@ pub(crate) fn event_sanitizer_callback_context_id(env: &Env) -> napi::Result napi::Result> { +pub(crate) fn callback_scope_stack( + env: &Env, +) -> napi::Result)>> { let factories = callback_factories(env)?; let callback: JsFunction = factories.get_named_property("callbackScopeStack")?; let value = callback.call::(None, &[])?; @@ -298,7 +301,10 @@ pub(crate) fn callback_scope_stack(env: &Env) -> napi::Result::from_napi_value(env.raw(), value.raw())? }; - Ok(Some(stack.inner.clone())) + Ok(Some(( + stack.inner.clone(), + stack.publication_buffer.clone(), + ))) } pub(crate) fn with_callback_scope_stack( @@ -308,7 +314,14 @@ pub(crate) fn with_callback_scope_stack( ) -> napi::Result> { let factories = callback_factories(env)?; let with_stack: JsFunction = factories.get_named_property("withCallbackScopeStack")?; - let stack = ScopeStack::from(stack.inner.clone()).into_instance(*env)?; + let publication_buffer = callback_scope_stack(env)? + .and_then(|(_, buffer)| buffer) + .or_else(|| stack.publication_buffer.clone()); + let stack = ScopeStack { + inner: stack.inner.clone(), + publication_buffer, + } + .into_instance(*env)?; let outcome = with_stack.call(None, &[as_unknown(env, &stack), as_unknown(env, callback)])?; let outcome = unsafe { JsObject::from_raw_unchecked(env.raw(), outcome.raw()) }; if !outcome.get_named_property::("active")? { @@ -320,7 +333,14 @@ pub(crate) fn with_callback_scope_stack( pub(crate) fn set_callback_scope_stack(env: &Env, stack: &ScopeStack) -> napi::Result { let factories = callback_factories(env)?; let set_stack: JsFunction = factories.get_named_property("setCallbackScopeStack")?; - let stack = ScopeStack::from(stack.inner.clone()).into_instance(*env)?; + let publication_buffer = callback_scope_stack(env)? + .and_then(|(_, buffer)| buffer) + .or_else(|| stack.publication_buffer.clone()); + let stack = ScopeStack { + inner: stack.inner.clone(), + publication_buffer, + } + .into_instance(*env)?; set_stack .call::(None, &[as_unknown(env, &stack)])? .coerce_to_bool()? diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index 94b27ecca..8ee3ad7f6 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -20,6 +20,9 @@ use napi::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}; use napi::{Env, JsFunction, JsUnknown, NapiRaw, NapiValue}; use serde_json::Value as Json; +use nemo_relay::api::runtime::subscriber_dispatcher::{ + PublicationBuffer, capture_nested_publication_buffer, with_task_nested_publication_buffer, +}; use nemo_relay::api::runtime::{ScopeStackHandle, TASK_SCOPE_STACK, current_scope_stack}; use nemo_relay::error::{FlowError, Result as FlowResult}; @@ -32,11 +35,14 @@ tokio::task_local! { pub(crate) async fn with_publication_callback_context( context_id: Option, + publication_buffer: Option, future: F, ) -> F::Output { - PUBLICATION_CALLBACK_CONTEXT_ID - .scope(context_id, future) - .await + with_task_nested_publication_buffer( + publication_buffer, + PUBLICATION_CALLBACK_CONTEXT_ID.scope(context_id, future), + ) + .await } fn publication_callback_context_id() -> Option { @@ -80,6 +86,7 @@ struct CallArgs { publication_context_id: Option, /// Scope stack captured when Relay invokes the middleware. scope_stack: Option, + publication_buffer: Option, completion: CallCompletion, } @@ -156,6 +163,7 @@ fn build_next_unknown( next: NextFn, scope_stack: ScopeStackHandle, publication_context_id: Option, + publication_buffer: Option, ) -> napi::Result { let next_fn = match next { NextFn::Json(next) => { @@ -164,17 +172,22 @@ fn build_next_unknown( let next = next.clone(); let scope_stack = scope_stack.clone(); let publication_context_id = publication_context_id.clone(); + let publication_buffer = publication_buffer.clone(); ctx.env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - next(arg) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + next(arg) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + }) + .await + }, + ) .await }, |_env, value| Ok(value), @@ -187,17 +200,22 @@ fn build_next_unknown( let next = next.clone(); let scope_stack = scope_stack.clone(); let publication_context_id = publication_context_id.clone(); + let publication_buffer = publication_buffer.clone(); ctx.env.execute_tokio_future( async move { - with_publication_callback_context(publication_context_id, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - next(arg) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - }) - .await - }) + with_publication_callback_context( + publication_context_id, + publication_buffer, + async move { + TASK_SCOPE_STACK + .scope(scope_stack, async move { + next(arg) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + }) + .await + }, + ) .await }, |_env, value| Ok(value), @@ -271,6 +289,7 @@ impl PromiseAwareFn { next, scope_stack, ctx.value.publication_context_id.clone(), + ctx.value.publication_buffer.clone(), )? } None => undefined_to_unknown(&ctx.env)?, @@ -299,7 +318,11 @@ impl PromiseAwareFn { }; let scope_stack = match ctx.value.scope_stack { Some(scope_stack) => { - let scope_stack = ScopeStack::from(scope_stack).into_instance(ctx.env)?; + let scope_stack = ScopeStack { + inner: scope_stack, + publication_buffer: ctx.value.publication_buffer, + } + .into_instance(ctx.env)?; unsafe { JsUnknown::from_raw_unchecked(ctx.env.raw(), scope_stack.raw()) } } None => undefined_to_unknown(&ctx.env)?, @@ -438,6 +461,7 @@ impl PromiseAwareFn { // Scope identity applies to every middleware callback. The // publication bit controls only re-entrant flush behavior. scope_stack: Some(current_scope_stack()), + publication_buffer: capture_nested_publication_buffer(), completion: CallCompletion::new(sender), }), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, diff --git a/crates/node/src/types/mod.rs b/crates/node/src/types/mod.rs index 63c594f30..2a36c4dc9 100644 --- a/crates/node/src/types/mod.rs +++ b/crates/node/src/types/mod.rs @@ -8,6 +8,7 @@ //! Doc comments on `#[napi]` items are emitted into the generated `index.d.ts`. use napi_derive::napi; +use nemo_relay::api::runtime::subscriber_dispatcher::PublicationBuffer; use nemo_relay::api::runtime::{ScopeStackHandle, create_scope_stack}; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -94,6 +95,7 @@ impl From for ScopeType { #[napi] pub struct ScopeStack { pub(crate) inner: ScopeStackHandle, + pub(crate) publication_buffer: Option, } #[napi] @@ -103,13 +105,17 @@ impl ScopeStack { pub fn new() -> Self { Self { inner: create_scope_stack(), + publication_buffer: None, } } } impl From for ScopeStack { fn from(h: ScopeStackHandle) -> Self { - Self { inner: h } + Self { + inner: h, + publication_buffer: None, + } } } diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index a60b67454..e74a37b5a 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -129,6 +129,45 @@ describe('event sanitizer registries', () => { assert.deepEqual(events.at(-1).data, { sanitized: true }); }); + it('publishes nested Promise sanitizer events before already queued events', async () => { + const events = capture('node-event-sanitize-nested-order-sub'); + let sanitizerEntered; + const entered = new Promise((resolve) => { + sanitizerEntered = resolve; + }); + let releaseSanitizer; + const release = new Promise((resolve) => { + releaseSanitizer = resolve; + }); + lib.registerMarkSanitizeGuardrail( + 'node-event-sanitize-nested-order', + 0, + async (event, fields) => { + if (event.name === 'node-outer-event') { + sanitizerEntered(); + await release; + lib.withScopeStack(lib.createScopeStack(), () => lib.event('node-nested-event')); + } + return fields; + }, + ); + try { + lib.event('node-outer-event'); + await entered; + lib.event('node-later-event'); + releaseSanitizer(); + await lib.flushSubscribers(); + await waitFor(events, 3); + } finally { + lib.deregisterMarkSanitizeGuardrail('node-event-sanitize-nested-order'); + lib.deregisterSubscriber('node-event-sanitize-nested-order-sub'); + } + assert.deepEqual( + events.map((event) => event.name), + ['node-outer-event', 'node-nested-event', 'node-later-event'], + ); + }); + it('preserves the emitting scope stack across queued sanitizer awaits', async () => { const events = capture('node-event-sanitize-scope-context-sub'); const originalStack = lib.currentScopeStack(); diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 2a55ed5c4..6f4fefd7e 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -16,7 +16,8 @@ use nemo_relay::api::llm as core_llm_api; use nemo_relay::api::llm::LlmAttributes; use nemo_relay::api::registry as core_registry_api; use nemo_relay::api::runtime::subscriber_dispatcher::{ - with_publication_context, with_task_publication_context, + capture_nested_publication_buffer, sync_thread_publication_buffer, with_publication_context, + with_task_nested_publication_buffer, with_task_publication_context, }; use nemo_relay::api::runtime::{ LlmExecutionNextFn, LlmJsonStream, LlmStreamExecutionNextFn, ToolExecutionNextFn, @@ -102,6 +103,7 @@ where { let scope_stack = current_scope_stack_handle(); let publication_context = py_callable::capture_python_publication_context(); + let publication_buffer = capture_nested_publication_buffer(); if !python_event_loop_running(py)? { let result = py .detach(|| { @@ -114,9 +116,12 @@ where return convert(py, result).map(|value| value.into_bound(py)); } pyo3_async_runtimes::tokio::future_into_py(py, async move { - let result = with_task_publication_context( - publication_context, - TASK_SCOPE_STACK.scope(scope_stack, future), + let result = with_task_nested_publication_buffer( + publication_buffer, + with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, future), + ), ) .await .map_err(to_py_err)?; @@ -234,7 +239,10 @@ pub(crate) async fn forward_stream_to_channel( /// A ``ScopeStack`` that can be used for per-request or per-task isolation. #[pyfunction] pub fn create_scope_stack() -> PyScopeStack { - PyScopeStack(create_scope_stack_handle()) + PyScopeStack { + inner: create_scope_stack_handle(), + publication_buffer: None, + } } /// Capture a transport-neutral context from the current Relay scope stack. @@ -265,7 +273,10 @@ pub fn create_scope_stack_from_propagation( context: &PyPropagationContext, ) -> PyResult { create_scope_stack_from_propagation_handle(&context.inner) - .map(PyScopeStack) + .map(|inner| PyScopeStack { + inner, + publication_buffer: None, + }) .map_err(to_py_err) } @@ -279,20 +290,30 @@ pub fn create_scope_stack_from_propagation( /// stack: The ``ScopeStack`` to bind to the current thread. #[pyfunction] pub fn set_thread_scope_stack(stack: &PyScopeStack) { - bind_thread_scope_stack(stack.0.clone()); + bind_thread_scope_stack(stack.inner.clone()); + sync_thread_publication_buffer( + stack + .publication_buffer + .clone() + .or_else(capture_nested_publication_buffer), + ); } /// Capture the scope stack currently installed in native thread-local storage. #[pyfunction] pub fn capture_thread_scope_stack() -> PyThreadScopeStackBinding { - PyThreadScopeStackBinding(capture_thread_scope_stack_handle()) + PyThreadScopeStackBinding { + inner: capture_thread_scope_stack_handle(), + publication_buffer: capture_nested_publication_buffer(), + } } /// Restore a complete native thread binding captured by /// [`capture_thread_scope_stack`]. #[pyfunction] pub fn restore_thread_scope_stack(binding: &PyThreadScopeStackBinding) { - restore_thread_scope_stack_handle(binding.0.clone()); + restore_thread_scope_stack_handle(binding.inner.clone()); + sync_thread_publication_buffer(binding.publication_buffer.clone()); } /// Sync a ``ScopeStack`` to the current thread's Rust thread-local storage @@ -303,7 +324,13 @@ pub fn restore_thread_scope_stack(binding: &PyThreadScopeStackBinding) { /// affecting ``scope_stack_active()``. #[pyfunction] pub fn sync_thread_scope_stack(stack: &PyScopeStack) { - sync_bound_thread_scope_stack(stack.0.clone()); + sync_bound_thread_scope_stack(stack.inner.clone()); + sync_thread_publication_buffer( + stack + .publication_buffer + .clone() + .or_else(capture_nested_publication_buffer), + ); } /// Return whether the current execution context has an explicitly-initialized @@ -687,25 +714,29 @@ fn tool_call_execute<'py>( let scope_stack = current_scope_stack_handle(); let publication_context = py_callable::capture_python_publication_context(); + let publication_buffer = capture_nested_publication_buffer(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - with_task_publication_context( - publication_context, - TASK_SCOPE_STACK.scope(scope_stack, async move { - let result = core_tool_api::tool_call_execute( - core_tool_api::ToolCallExecuteParams::builder() - .name(name) - .args(args_json) - .func(default_fn) - .parent(parent_handle) - .attributes(attrs) - .data_opt(data_json) - .metadata_opt(metadata_json) - .build(), - ) - .await - .map_err(to_py_err)?; - Python::attach(|py| json_to_py(py, &result)) - }), + with_task_nested_publication_buffer( + publication_buffer, + with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, async move { + let result = core_tool_api::tool_call_execute( + core_tool_api::ToolCallExecuteParams::builder() + .name(name) + .args(args_json) + .func(default_fn) + .parent(parent_handle) + .attributes(attrs) + .data_opt(data_json) + .metadata_opt(metadata_json) + .build(), + ) + .await + .map_err(to_py_err)?; + Python::attach(|py| json_to_py(py, &result)) + }), + ), ) .await }) @@ -919,27 +950,31 @@ fn llm_call_execute<'py>( let scope_stack = current_scope_stack_handle(); let publication_context = py_callable::capture_python_publication_context(); + let publication_buffer = capture_nested_publication_buffer(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - with_task_publication_context( - publication_context, - TASK_SCOPE_STACK.scope(scope_stack, async move { - let params = core_llm_api::LlmCallExecuteParams::builder() - .name(name) - .request(request.inner) - .func(default_fn) - .parent(parent_handle) - .attributes(attrs) - .data_opt(data_json) - .metadata_opt(metadata_json) - .model_name_opt(model_name) - .codec_opt(codec_arc) - .response_codec_opt(response_codec_arc) - .build(); - let result = core_llm_api::llm_call_execute(params) - .await - .map_err(to_py_err)?; - Python::attach(|py| json_to_py(py, &result)) - }), + with_task_nested_publication_buffer( + publication_buffer, + with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, async move { + let params = core_llm_api::LlmCallExecuteParams::builder() + .name(name) + .request(request.inner) + .func(default_fn) + .parent(parent_handle) + .attributes(attrs) + .data_opt(data_json) + .metadata_opt(metadata_json) + .model_name_opt(model_name) + .codec_opt(codec_arc) + .response_codec_opt(response_codec_arc) + .build(); + let result = core_llm_api::llm_call_execute(params) + .await + .map_err(to_py_err)?; + Python::attach(|py| json_to_py(py, &result)) + }), + ), ) .await }) @@ -1024,43 +1059,51 @@ fn llm_stream_call_execute<'py>( let scope_stack = current_scope_stack_handle(); let publication_context = py_callable::capture_python_publication_context(); let stream_publication_context = publication_context.clone(); + let publication_buffer = capture_nested_publication_buffer(); + let stream_publication_buffer = publication_buffer.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - with_task_publication_context( - publication_context, - TASK_SCOPE_STACK.scope(scope_stack, async move { - let params = core_llm_api::LlmStreamCallExecuteParams::builder() - .name(name) - .request(request.inner) - .func(default_fn) - .collector(collector_fn) - .finalizer(finalizer_fn) - .parent(parent_handle) - .attributes(attrs) - .data_opt(data_json) - .metadata_opt(metadata_json) - .model_name_opt(model_name) - .codec_opt(codec_arc) - .response_codec_opt(response_codec_arc) - .build(); - let rust_stream = core_llm_api::llm_stream_call_execute(params) - .await - .map_err(to_py_err)?; - - // Spawn a tokio task that drains the Rust stream into an mpsc channel - let (tx, rx) = tokio::sync::mpsc::channel::>(32); - let (cancel, cancel_rx) = tokio::sync::watch::channel(false); - let (closed, closed_rx) = tokio::sync::watch::channel(None); - tokio::spawn(with_task_publication_context( - stream_publication_context, - forward_stream_to_channel(rust_stream, tx, cancel_rx, closed), - )); - - Ok(PyLlmStream { - receiver: Arc::new(tokio::sync::Mutex::new(rx)), - cancel, - closed: closed_rx, - }) - }), + with_task_nested_publication_buffer( + publication_buffer, + with_task_publication_context( + publication_context, + TASK_SCOPE_STACK.scope(scope_stack, async move { + let params = core_llm_api::LlmStreamCallExecuteParams::builder() + .name(name) + .request(request.inner) + .func(default_fn) + .collector(collector_fn) + .finalizer(finalizer_fn) + .parent(parent_handle) + .attributes(attrs) + .data_opt(data_json) + .metadata_opt(metadata_json) + .model_name_opt(model_name) + .codec_opt(codec_arc) + .response_codec_opt(response_codec_arc) + .build(); + let rust_stream = core_llm_api::llm_stream_call_execute(params) + .await + .map_err(to_py_err)?; + + // Spawn a tokio task that drains the Rust stream into an mpsc channel + let (tx, rx) = tokio::sync::mpsc::channel::>(32); + let (cancel, cancel_rx) = tokio::sync::watch::channel(false); + let (closed, closed_rx) = tokio::sync::watch::channel(None); + tokio::spawn(with_task_nested_publication_buffer( + stream_publication_buffer, + with_task_publication_context( + stream_publication_context, + forward_stream_to_channel(rust_stream, tx, cancel_rx, closed), + ), + )); + + Ok(PyLlmStream { + receiver: Arc::new(tokio::sync::Mutex::new(rx)), + cancel, + closed: closed_rx, + }) + }), + ), ) .await }) diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 9ccd34b81..d3881fe73 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -25,7 +25,9 @@ use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; -use nemo_relay::api::runtime::subscriber_dispatcher::{PublicationContext, publication_context}; +use nemo_relay::api::runtime::subscriber_dispatcher::{ + PublicationBuffer, PublicationContext, capture_nested_publication_buffer, publication_context, +}; use nemo_relay::api::runtime::{ EventSanitizeFn, EventSubscriberFn, LlmConditionalFn, LlmExecutionNextFn, LlmJsonStream, LlmRequestInterceptFn, LlmSanitizeRequestContext, LlmSanitizeRequestFn, @@ -409,9 +411,29 @@ fn copy_publication_invocation<'py>( py: Python<'py>, context: &PythonPublicationContext, fallback_task_locals: Option, +) -> PyResult<(Bound<'py, PyAny>, Option)> { + copy_publication_invocation_with_buffer( + py, + context, + fallback_task_locals, + capture_nested_publication_buffer(), + ) +} + +fn copy_publication_invocation_with_buffer<'py>( + py: Python<'py>, + context: &PythonPublicationContext, + fallback_task_locals: Option, + publication_buffer: Option, ) -> PyResult<(Bound<'py, PyAny>, Option)> { let invocation_context = context.context.bind(py).call_method0("copy")?; - let scope_stack = Py::new(py, PyScopeStack(context.scope_stack.clone()))?; + let scope_stack = Py::new( + py, + PyScopeStack { + inner: context.scope_stack.clone(), + publication_buffer, + }, + )?; let nemo_relay = py.import("nemo_relay")?; if let Ok(scope_stack_var) = nemo_relay.getattr("_scope_stack_var") { invocation_context.call_method1("run", (scope_stack_var.getattr("set")?, scope_stack))?; @@ -750,17 +772,23 @@ pub fn wrap_py_tool_fn(py_fn: Py) -> ToolSanitizeFn { let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); let publication_context = publication_context::(); + let publication_buffer = capture_nested_publication_buffer(); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context, task_locals) - .map_err(|error| FlowError::Internal(error.to_string()))?; + copy_publication_invocation_with_buffer( + py, + context, + task_locals, + publication_buffer.clone(), + ) + .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => copy_middleware_invocation(py, task_locals) + None => { copy_middleware_invocation(py, task_locals) } .map_err(|error| FlowError::Internal(error.to_string()))?, }; let py_args = json_to_py(py, &args) @@ -1186,6 +1214,7 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); let publication_context = publication_context::(); + let publication_buffer = capture_nested_publication_buffer(); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { @@ -1193,11 +1222,16 @@ fn wrap_py_llm_sanitize_request_callback(py_fn: Py) -> LlmSanitizeRequest let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context, task_locals) - .map_err(|error| FlowError::Internal(error.to_string()))?; + copy_publication_invocation_with_buffer( + py, + context, + task_locals, + publication_buffer.clone(), + ) + .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => copy_middleware_invocation(py, task_locals) + None => { copy_middleware_invocation(py, task_locals) } .map_err(|error| FlowError::Internal(error.to_string()))?, }; let args = ( @@ -1499,17 +1533,23 @@ fn wrap_py_llm_sanitize_response_callback(py_fn: Py) -> LlmSanitizeRespon let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); let publication_context = publication_context::(); + let publication_buffer = capture_nested_publication_buffer(); let publication = nemo_relay::api::runtime::subscriber_dispatcher::in_dispatcher_callback(); Box::pin(async move { let result = resolve_py_object_or_future(Python::attach(|py| { let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context, task_locals) - .map_err(|error| FlowError::Internal(error.to_string()))?; + copy_publication_invocation_with_buffer( + py, + context, + task_locals, + publication_buffer.clone(), + ) + .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => copy_middleware_invocation(py, task_locals) + None => { copy_middleware_invocation(py, task_locals) } .map_err(|error| FlowError::Internal(error.to_string()))?, }; let py_context = PyLlmSanitizeResponseContext { inner: context }; @@ -1607,17 +1647,23 @@ pub fn wrap_py_event_sanitize_fn(py_fn: Py) -> EventSanitizeFn { let py_fn = py_fn.clone(); let task_locals = task_locals_with_running_loop(task_locals.as_ref()); let publication_context = publication_context::(); + let publication_buffer = capture_nested_publication_buffer(); Box::pin(async move { let result = Python::attach( |py| -> FlowResult, PyValueFuture>> { let (invocation_context, task_locals) = match publication_context.as_ref() { Some(context) => { let (context, publication_task_locals) = - copy_publication_invocation(py, context, task_locals) - .map_err(|error| FlowError::Internal(error.to_string()))?; + copy_publication_invocation_with_buffer( + py, + context, + task_locals, + publication_buffer.clone(), + ) + .map_err(|error| FlowError::Internal(error.to_string()))?; (Some(context), publication_task_locals) } - None => copy_middleware_invocation(py, task_locals) + None => { copy_middleware_invocation(py, task_locals) } .map_err(|error| FlowError::Internal(error.to_string()))?, }; let py_event = match event.as_ref() { diff --git a/crates/python/src/py_types/core.rs b/crates/python/src/py_types/core.rs index 3eb57bf5a..a798ad932 100644 --- a/crates/python/src/py_types/core.rs +++ b/crates/python/src/py_types/core.rs @@ -14,6 +14,7 @@ use super::{ }; use nemo_relay::api::event::{CategoryProfile, EventCategory, PendingMarkSpec}; use nemo_relay::api::llm::LlmRequestInterceptOutcome; +use nemo_relay::api::runtime::subscriber_dispatcher::PublicationBuffer; use nemo_relay::api::runtime::{ LlmSanitizeRequestContext, LlmSanitizeResponseContext, PropagationContext, ThreadScopeStackBinding, @@ -182,7 +183,10 @@ impl Drop for PyLlmStream { /// Each ``ScopeStack`` wraps an independent scope stack with its own root /// scope. Use ``create_scope_stack()`` to obtain one. #[pyclass(name = "ScopeStack")] -pub struct PyScopeStack(pub ScopeStackHandle); +pub struct PyScopeStack { + pub(crate) inner: ScopeStackHandle, + pub(crate) publication_buffer: Option, +} #[pymethods] impl PyScopeStack { @@ -193,7 +197,10 @@ impl PyScopeStack { /// Opaque captured native thread binding used to restore a Python scope context. #[pyclass(name = "_ThreadScopeStackBinding")] -pub struct PyThreadScopeStackBinding(pub ThreadScopeStackBinding); +pub struct PyThreadScopeStackBinding { + pub(crate) inner: ThreadScopeStackBinding, + pub(crate) publication_buffer: Option, +} #[pymethods] impl PyThreadScopeStackBinding { diff --git a/crates/python/tests/coverage/py_types_coverage_tests.rs b/crates/python/tests/coverage/py_types_coverage_tests.rs index 038632356..abebfbc3f 100644 --- a/crates/python/tests/coverage/py_types_coverage_tests.rs +++ b/crates/python/tests/coverage/py_types_coverage_tests.rs @@ -528,7 +528,10 @@ fn test_stream_request_event_and_handle_wrappers_cover_remaining_methods() { assert_eq!(llm_and.value(), PyLLMAttributes::STREAMING); Python::attach(|py| { - let stack = PyScopeStack(nemo_relay::api::runtime::create_scope_stack()); + let stack = PyScopeStack { + inner: nemo_relay::api::runtime::create_scope_stack(), + publication_buffer: None, + }; assert_eq!(stack.__repr__(), ""); let parent_uuid = Uuid::now_v7(); diff --git a/python/nemo_relay/__init__.py b/python/nemo_relay/__init__.py index 7d88c1b14..cdcf01dd7 100644 --- a/python/nemo_relay/__init__.py +++ b/python/nemo_relay/__init__.py @@ -431,6 +431,9 @@ def create_scope_stack_from_propagation(context: PropagationContext) -> ScopeSta @contextmanager def use_scope_stack(stack: ScopeStack): """Temporarily install ``stack`` in the current Python context.""" + current_stack = _scope_stack_var.get(None) + if current_stack is not None: + _sync_thread_scope_stack(current_stack) previous_native_stack = _capture_thread_scope_stack() token = _scope_stack_var.set(stack) _sync_thread_scope_stack(stack) diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 18acbfdec..1cf889c9c 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -100,6 +100,36 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert events[-1].data == {"async": True} +async def test_nested_async_sanitizer_event_precedes_already_queued_event(capture_events): + _capture_name, events = capture_events + entered = asyncio.Event() + release = asyncio.Event() + + async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + if event.name == "python-outer-event": + entered.set() + await release.wait() + with nemo_relay.use_scope_stack(nemo_relay.create_scope_stack()): + scope.event("python-nested-event") + return fields + + guardrails.register_mark_sanitize("python-nested-event-order", 0, sanitize) + try: + scope.event("python-outer-event") + await entered.wait() + scope.event("python-later-event") + release.set() + await subscribers.flush_async() + finally: + guardrails.deregister_mark_sanitize("python-nested-event-order") + + assert [event.name for event in events] == [ + "python-outer-event", + "python-nested-event", + "python-later-event", + ] + + async def test_async_mark_sanitizer_uses_each_emitter_context(capture_events): request_id = contextvars.ContextVar("request_id", default="registration") observed: dict[str, str] = {} From ed34e0bce8be8184e00b21d1c49d7096b360a534 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 09:37:03 -0400 Subject: [PATCH 56/83] fix(runtime): preserve nested async publication context Signed-off-by: Will Killian --- crates/core/src/api/runtime/state.rs | 29 ++- .../src/api/runtime/subscriber_dispatcher.rs | 32 +++- crates/core/src/api/tool.rs | 47 ++++- crates/core/src/plugin/dynamic/worker.rs | 56 +++++- .../tests/fixtures/worker_plugin/src/main.rs | 18 ++ .../tests/integration/middleware_tests.rs | 178 +++++++++++++++++- .../tests/integration/worker_plugin_tests.rs | 48 +++++ .../core/tests/unit/dynamic_worker_tests.rs | 4 +- .../tests/unit/subscriber_dispatcher_tests.rs | 39 +++- crates/node/src/api/mod.rs | 20 +- crates/node/tests/event_sanitizers_tests.mjs | 45 +++++ crates/python/src/lib.rs | 1 + crates/python/src/py_api/mod.rs | 9 +- crates/python/src/py_callable.rs | 6 +- .../test_support.rs => tests/support/mod.rs} | 4 +- python/tests/test_event_sanitizers.py | 63 +++++++ 16 files changed, 547 insertions(+), 52 deletions(-) rename crates/python/{src/test_support.rs => tests/support/mod.rs} (96%) diff --git a/crates/core/src/api/runtime/state.rs b/crates/core/src/api/runtime/state.rs index ebfc8b112..62b4b4b08 100644 --- a/crates/core/src/api/runtime/state.rs +++ b/crates/core/src/api/runtime/state.rs @@ -23,6 +23,7 @@ use crate::api::event::{ use crate::api::llm::{CreateLlmHandleParams, EndLlmHandleParams}; use crate::api::llm::{LlmHandle, LlmRequest}; use crate::api::registry::{ExecutionIntercept, Guardrail, Intercept}; +use crate::api::runtime::ScopeStackHandle; use crate::api::runtime::callbacks::{ EventSanitizeFn, EventSubscriberFn, LlmConditionalFn, LlmExecutionFn, LlmExecutionNextFn, LlmRequestInterceptFn, LlmSanitizeRequestContext, LlmSanitizeRequestFn, @@ -52,19 +53,30 @@ use uuid::Uuid; struct GuardrailScopeCompletion<'a> { handle: Option, subscribers: &'a [EventSubscriberFn], + scope_stack: ScopeStackHandle, } impl GuardrailScopeCompletion<'_> { - fn new(handle: ScopeHandle, subscribers: &[EventSubscriberFn]) -> GuardrailScopeCompletion<'_> { + fn new( + handle: ScopeHandle, + subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, + ) -> GuardrailScopeCompletion<'_> { GuardrailScopeCompletion { handle: Some(handle), subscribers, + scope_stack, } } fn finish(mut self, output: Json) { let handle = self.handle.take().expect("guardrail scope handle"); - NemoRelayContextState::emit_guardrail_scope_end(&handle, output, self.subscribers); + NemoRelayContextState::emit_guardrail_scope_end( + &handle, + output, + self.subscribers, + self.scope_stack.clone(), + ); } } @@ -81,6 +93,7 @@ impl Drop for GuardrailScopeCompletion<'_> { "error": "guardrail evaluation cancelled", }), self.subscribers, + self.scope_stack.clone(), ); } } @@ -602,6 +615,7 @@ impl NemoRelayContextState { metadata: Option, input: Json, subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, ) -> ScopeHandle { let handle = ScopeHandle::builder() .name(name) @@ -623,7 +637,6 @@ impl NemoRelayContextState { EventCategory::from(handle.scope_type), None, )); - let scope_stack = super::current_scope_stack(); let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); subscriber_dispatcher::dispatch_sanitized_event( event, @@ -638,6 +651,7 @@ impl NemoRelayContextState { handle: &ScopeHandle, output: Json, subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, ) { let event = Event::Scope(ScopeEvent::new( BaseEvent::builder() @@ -653,7 +667,6 @@ impl NemoRelayContextState { EventCategory::from(handle.scope_type), None, )); - let scope_stack = super::current_scope_stack(); let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); subscriber_dispatcher::dispatch_sanitized_event( event, @@ -887,6 +900,7 @@ impl NemoRelayContextState { metadata: Option, ) -> crate::error::Result> { for entry in entries { + let scope_stack = super::current_scope_stack(); let handle = Self::emit_guardrail_scope_start( &entry.name, parent_uuid, @@ -896,8 +910,9 @@ impl NemoRelayContextState { "target_name": name, }), subscribers, + scope_stack.clone(), ); - let completion = GuardrailScopeCompletion::new(handle, subscribers); + let completion = GuardrailScopeCompletion::new(handle, subscribers, scope_stack); let callback = Arc::clone(&entry.payload); let callback_name = name.to_string(); let callback_args = args.clone(); @@ -1265,6 +1280,7 @@ impl NemoRelayContextState { metadata: Option, ) -> crate::error::Result> { for entry in entries { + let scope_stack = super::current_scope_stack(); let handle = Self::emit_guardrail_scope_start( &entry.name, parent_uuid, @@ -1273,8 +1289,9 @@ impl NemoRelayContextState { "kind": "llm_conditional_execution", }), subscribers, + scope_stack.clone(), ); - let completion = GuardrailScopeCompletion::new(handle, subscribers); + let completion = GuardrailScopeCompletion::new(handle, subscribers, scope_stack); let callback = Arc::clone(&entry.payload); let callback_request = request.clone(); let result = match AssertUnwindSafe(async move { callback(callback_request).await }) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index f5568c3a1..bbdfea04c 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -715,7 +715,7 @@ mod native { /// failure drops the event because it may be responsible for inserting the /// sanitized payload. A sanitizer failure retains the transformed snapshot /// and continues publication (fail open). - fn sanitize_event_snapshot( + pub(super) fn sanitize_event_snapshot( event: Event, transform: Option, sanitizers: Vec>, @@ -737,10 +737,23 @@ mod native { log::error!( target: "nemo_relay.runtime", event = "event_sanitizer_runtime_failed"; - "Event sanitizer runtime failed; dropping events: {error}" + "Event sanitizer runtime failed: {error}" ); } - return (None, Vec::new()); + if transform.is_some() { + log::error!( + target: "nemo_relay.runtime", + event = "event_transform_runtime_unavailable"; + "Dropping an event because its required asynchronous transform could not run" + ); + return (None, Vec::new()); + } + log::error!( + target: "nemo_relay.runtime", + event = "event_sanitizer_fail_open"; + "Publishing the original event snapshot because event sanitizers could not run" + ); + return (Some(event), Vec::new()); } }; let transform_context = publication_context.clone(); @@ -820,6 +833,19 @@ mod native { PROCESS_STATE.store(state, Ordering::Release); }); } + + #[cfg(test)] + pub(super) fn set_sanitizer_runtime_failure_for_test(error: Option<&str>) { + let state = process_state(); + let mut runtime = state + .sanitizer_runtime + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + *runtime = error.map(|error| Err(error.to_string())); + state + .sanitizer_runtime_failure_logged + .store(false, Ordering::Release); + } } #[cfg(test)] diff --git a/crates/core/src/api/tool.rs b/crates/core/src/api/tool.rs index 5333928c4..e983b4468 100644 --- a/crates/core/src/api/tool.rs +++ b/crates/core/src/api/tool.rs @@ -10,7 +10,9 @@ use crate::api::runtime::global_context; use crate::api::runtime::subscriber_dispatcher::{ dispatch_sanitized_event, dispatch_transformed_event, }; -use crate::api::runtime::{EventSubscriberFn, ToolExecutionNextFn, with_active_event_uuid}; +use crate::api::runtime::{ + EventSubscriberFn, ScopeStackHandle, ToolExecutionNextFn, with_active_event_uuid, +}; use crate::api::scope::event; use crate::api::scope::{EmitMarkEventParams, ScopeHandle}; use crate::api::shared::{ @@ -29,6 +31,14 @@ pub use nemo_relay_types::api::tool::{ToolAttributes, ToolExecutionInterceptOutc fn queue_sanitized_event(event: Event, subscribers: &[EventSubscriberFn]) -> bool { let scope_stack = current_scope_stack(); + queue_sanitized_event_with_scope_stack(event, subscribers, scope_stack) +} + +fn queue_sanitized_event_with_scope_stack( + event: Event, + subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, +) -> bool { let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default(); dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack) } @@ -566,6 +576,7 @@ fn emit_tool_end_without_output( handle: &ToolHandle, metadata: Option, lifecycle_subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, ) -> Result<()> { ensure_runtime_owner()?; let event = { @@ -575,7 +586,7 @@ fn emit_tool_end_without_output( .map_err(|error| FlowError::Internal(error.to_string()))?; state.end_tool_handle(handle, handle.data.clone(), metadata) }; - queue_sanitized_event(event, lifecycle_subscribers); + queue_sanitized_event_with_scope_stack(event, lifecycle_subscribers, scope_stack); Ok(()) } @@ -583,14 +594,21 @@ struct ManagedToolCompletion { handle: Option, metadata: Option, subscribers: Vec, + scope_stack: ScopeStackHandle, } impl ManagedToolCompletion { - fn new(handle: &ToolHandle, metadata: Option, subscribers: &[EventSubscriberFn]) -> Self { + fn new( + handle: &ToolHandle, + metadata: Option, + subscribers: &[EventSubscriberFn], + scope_stack: ScopeStackHandle, + ) -> Self { Self { handle: Some(handle.clone()), metadata, subscribers: subscribers.to_vec(), + scope_stack, } } @@ -609,7 +627,12 @@ impl Drop for ManagedToolCompletion { "ERROR", Some("tool execution cancelled".into()), ); - let _ = emit_tool_end_without_output(&handle, metadata, &self.subscribers); + let _ = emit_tool_end_without_output( + &handle, + metadata, + &self.subscribers, + self.scope_stack.clone(), + ); } } @@ -742,8 +765,13 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { state.tool_build_execution_chain(&name, func, &scope_locals) }; - let mut completion = - ManagedToolCompletion::new(&handle, metadata.clone(), &lifecycle_subscribers); + let lifecycle_scope_stack = current_scope_stack(); + let mut completion = ManagedToolCompletion::new( + &handle, + metadata.clone(), + &lifecycle_subscribers, + lifecycle_scope_stack.clone(), + ); match with_active_event_uuid(handle.uuid, execution(intercepted_args)).await { Ok(outcome) => { let ToolExecutionInterceptOutcome { @@ -768,7 +796,12 @@ pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result { Err(error) => { let end_metadata = metadata_with_otel_status(metadata, "ERROR", Some(error.to_string())); - let _ = emit_tool_end_without_output(&handle, end_metadata, &lifecycle_subscribers); + let _ = emit_tool_end_without_output( + &handle, + end_metadata, + &lifecycle_subscribers, + lifecycle_scope_stack, + ); completion.disarm(); Err(error) } diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index b1cc8309e..b11bb431d 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -63,7 +63,9 @@ use crate::api::optimization::{ }; use crate::api::runtime::scope_stack::active_event_uuid; use crate::api::runtime::subscriber_dispatcher::{ - PublicationContext, capture_publication_context, with_task_publication_context, + PublicationBuffer, PublicationContext, capture_nested_publication_buffer, + capture_publication_context, with_nested_publication_buffer, + with_task_nested_publication_buffer, with_task_publication_context, }; use crate::api::runtime::{ EventSanitizeFn, LlmCodecIdentity, LlmExecutionNextFn, LlmJsonStream, @@ -1854,9 +1856,10 @@ impl WorkerPluginCallback { continuation_id: Option, payload: Option, ) -> InvokeRequest { - let scope_stack_id = self - .host_state - .insert_invocation_scope_stack(current_scope_stack()); + let scope_stack_id = self.host_state.insert_invocation_scope_stack( + current_scope_stack(), + capture_nested_publication_buffer(), + ); InvokeRequest { activation_id: self.activation_id.clone(), auth_token: self.host_state.auth_token.clone(), @@ -2110,6 +2113,7 @@ enum WorkerCodecDirection { struct StoredScopeStack { handle: crate::api::runtime::ScopeStackHandle, + publication_buffer: Option, invocation_base_depth: Option, } @@ -2249,6 +2253,7 @@ impl WorkerHostRuntimeState { fn insert_invocation_scope_stack( &self, stack: crate::api::runtime::ScopeStackHandle, + publication_buffer: Option, ) -> String { let id = format!("invoke-{}", Uuid::now_v7()); let Ok(mut stacks) = self.scope_stacks.lock() else { @@ -2280,6 +2285,7 @@ impl WorkerHostRuntimeState { id.clone(), StoredScopeStack { handle: stack, + publication_buffer, invocation_base_depth: Some(invocation_base_depth), }, ); @@ -2386,6 +2392,7 @@ impl WorkerHostRuntimeState { .ok_or_else(|| Status::not_found("continuation not found")) } + #[cfg(test)] fn stack(&self, id: &str) -> Result, Status> { if id.is_empty() { return Ok(None); @@ -2398,6 +2405,28 @@ impl WorkerHostRuntimeState { .map(Some) .ok_or_else(|| Status::not_found("scope stack not found")) } + + fn invocation_context(&self, id: &str) -> Result, Status> { + if id.is_empty() { + return Ok(None); + } + self.scope_stacks + .lock() + .map_err(|err| Status::internal(format!("scope stack lock poisoned: {err}")))? + .get(id) + .map(|stored| StoredInvocationContext { + scope_stack: stored.handle.clone(), + publication_buffer: stored.publication_buffer.clone(), + }) + .map(Some) + .ok_or_else(|| Status::not_found("scope stack not found")) + } +} + +#[derive(Clone)] +struct StoredInvocationContext { + scope_stack: crate::api::runtime::ScopeStackHandle, + publication_buffer: Option, } #[derive(Clone)] @@ -2405,6 +2434,7 @@ struct ContinuationContext { scope_stack: crate::api::runtime::ScopeStackHandle, active_event_uuid: Option, publication_context: Option, + publication_buffer: Option, optimization_recorder: Option, } @@ -2414,6 +2444,7 @@ impl ContinuationContext { scope_stack: current_scope_stack(), active_event_uuid: active_event_uuid(), publication_context: capture_publication_context(), + publication_buffer: capture_nested_publication_buffer(), optimization_recorder: current_llm_optimization_recorder(), } } @@ -2421,6 +2452,8 @@ impl ContinuationContext { async fn run(&self, future: F) -> F::Output { let scoped = TASK_SCOPE_STACK.scope(self.scope_stack.clone(), future); let published = with_task_publication_context(self.publication_context.clone(), scoped); + let published = + with_task_nested_publication_buffer(self.publication_buffer.clone(), published); let active = async { match self.active_event_uuid { Some(uuid) => with_active_event_uuid(uuid, published).await, @@ -2575,8 +2608,10 @@ impl RelayHostRuntime for WorkerHostRuntimeService { }; let result = if handle.scope_stack_id.is_empty() { pop() - } else if let Some(stack) = self.state.stack(&handle.scope_stack_id)? { - with_scope_stack(stack, pop) + } else if let Some(context) = self.state.invocation_context(&handle.scope_stack_id)? { + with_nested_publication_buffer(context.publication_buffer, || { + with_scope_stack(context.scope_stack, pop) + }) } else { pop() }; @@ -2599,6 +2634,7 @@ impl RelayHostRuntime for WorkerHostRuntimeService { id.clone(), StoredScopeStack { handle: crate::api::runtime::create_scope_stack(), + publication_buffer: None, invocation_base_depth: None, }, ); @@ -2809,14 +2845,16 @@ impl WorkerHostRuntimeService { let Some(stack_id) = scope.map(|scope| scope.scope_stack_id.as_str()) else { return f(); }; - let Some(stack) = self + let Some(context) = self .state - .stack(stack_id) + .invocation_context(stack_id) .map_err(|err| FlowError::Internal(err.to_string()))? else { return f(); }; - with_scope_stack(stack, f) + with_nested_publication_buffer(context.publication_buffer, || { + with_scope_stack(context.scope_stack, f) + }) } } diff --git a/crates/core/tests/fixtures/worker_plugin/src/main.rs b/crates/core/tests/fixtures/worker_plugin/src/main.rs index 10917e56b..a2276e123 100644 --- a/crates/core/tests/fixtures/worker_plugin/src/main.rs +++ b/crates/core/tests/fixtures/worker_plugin/src/main.rs @@ -72,6 +72,24 @@ impl WorkerPlugin for FixtureWorkerPlugin { Ok(fields) }, ); + let nested_publication_runtime = runtime.clone(); + ctx.register_mark_sanitize_guardrail( + "fixture_nested_publication_order", + 2, + move |event, fields| { + let runtime = nested_publication_runtime.clone(); + let emit_nested = event.name() == "worker-nested-order-outer"; + async move { + if emit_nested { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + runtime + .emit_mark("worker-nested-order-inner", None, None) + .await?; + } + Ok(fields) + } + }, + ); ctx.register_scope_sanitize_start_guardrail( "fixture_scope_start_sanitize", 0, diff --git a/crates/core/tests/integration/middleware_tests.rs b/crates/core/tests/integration/middleware_tests.rs index 9a8a205af..e6b1e0149 100644 --- a/crates/core/tests/integration/middleware_tests.rs +++ b/crates/core/tests/integration/middleware_tests.rs @@ -44,9 +44,9 @@ use nemo_relay::api::registry::{ scope_register_llm_conditional_execution_guardrail, scope_register_llm_execution_intercept, scope_register_llm_request_intercept, scope_register_llm_sanitize_request_guardrail, scope_register_llm_sanitize_response_guardrail, scope_register_llm_stream_execution_intercept, - scope_register_mark_sanitize_guardrail, scope_register_tool_conditional_execution_guardrail, - scope_register_tool_execution_intercept, scope_register_tool_request_intercept, - scope_register_tool_sanitize_request_guardrail, + scope_register_mark_sanitize_guardrail, scope_register_scope_sanitize_end_guardrail, + scope_register_tool_conditional_execution_guardrail, scope_register_tool_execution_intercept, + scope_register_tool_request_intercept, scope_register_tool_sanitize_request_guardrail, scope_register_tool_sanitize_response_guardrail, }; use nemo_relay::api::runtime::NemoRelayContextState; @@ -1376,6 +1376,92 @@ async fn dropping_pending_tool_execution_closes_the_managed_lifecycle() { deregister_subscriber("cancelled_tool_lifecycle").unwrap(); } +#[tokio::test] +async fn cancelled_tool_end_uses_the_originating_scope_sanitizer() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let originating_stack = current_scope_stack(); + let owner = push_scope( + nemo_relay::api::scope::PushScopeParams::builder() + .name("cancelled-tool-sanitizer-owner") + .scope_type(ScopeType::Agent) + .build(), + ) + .unwrap(); + scope_register_scope_sanitize_end_guardrail( + &owner.uuid, + "cancelled-tool-end-sanitizer", + 1, + Arc::new(|event, mut fields| { + if event.name() == "cancelled-tool-cross-scope" { + fields.data = Some(json!({"secret": "[redacted]"})); + } + ready(fields) + }), + ) + .unwrap(); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = events.clone(); + register_subscriber( + "cancelled_tool_cross_scope_observer", + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .unwrap(); + + let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); + let entered_tx = Arc::new(Mutex::new(Some(entered_tx))); + register_tool_execution_intercept( + "pending_tool_cross_scope", + 1, + Arc::new(move |_name, _args, _next| { + if let Some(sender) = entered_tx.lock().unwrap().take() { + let _ = sender.send(()); + } + Box::pin(std::future::pending()) + }), + ) + .unwrap(); + + let mut execution = Box::pin(tool_call_execute( + nemo_relay::api::tool::ToolCallExecuteParams::builder() + .name("cancelled-tool-cross-scope") + .args(json!({})) + .data(json!({"secret": "classified"})) + .func(Arc::new(|args| Box::pin(async move { Ok(args) }))) + .build(), + )); + tokio::select! { + result = &mut execution => panic!("execution unexpectedly completed: {result:?}"), + result = entered_rx => result.unwrap(), + } + + set_thread_scope_stack(create_scope_stack()); + drop(execution); + flush_subscribers().unwrap(); + + let end = captured_events_snapshot(&events) + .into_iter() + .find(|event| { + event.name() == "cancelled-tool-cross-scope" + && event.scope_category() == Some(ScopeCategory::End) + }) + .expect("cancelled tool should emit an end event"); + assert_eq!(end.data(), Some(&json!({"secret": "[redacted]"}))); + + set_thread_scope_stack(originating_stack); + deregister_tool_execution_intercept("pending_tool_cross_scope").unwrap(); + deregister_subscriber("cancelled_tool_cross_scope_observer").unwrap(); + pop_scope( + nemo_relay::api::scope::PopScopeParams::builder() + .handle_uuid(&owner.uuid) + .build(), + ) + .unwrap(); +} + #[tokio::test] async fn dropping_pending_conditional_closes_the_guardrail_scope() { let _lock = TEST_MUTEX.lock().unwrap(); @@ -1431,6 +1517,92 @@ async fn dropping_pending_conditional_closes_the_guardrail_scope() { deregister_subscriber("cancelled_guardrail_lifecycle").unwrap(); } +#[tokio::test] +async fn cancelled_guardrail_end_uses_the_originating_scope_sanitizer() { + let _lock = TEST_MUTEX.lock().unwrap(); + reset_global(); + setup_isolated_thread(); + + let originating_stack = current_scope_stack(); + let owner = push_scope( + nemo_relay::api::scope::PushScopeParams::builder() + .name("cancelled-guardrail-sanitizer-owner") + .scope_type(ScopeType::Agent) + .build(), + ) + .unwrap(); + scope_register_scope_sanitize_end_guardrail( + &owner.uuid, + "cancelled-guardrail-end-sanitizer", + 1, + Arc::new(|event, mut fields| { + if event.name() == "pending-conditional-cross-scope" { + fields.metadata = Some(json!({"sanitized_by": "originating-scope"})); + } + ready(fields) + }), + ) + .unwrap(); + + let events = Arc::new(Mutex::new(Vec::::new())); + let captured = events.clone(); + register_subscriber( + "cancelled_guardrail_cross_scope_observer", + Arc::new(move |event| captured.lock().unwrap().push(event.clone())), + ) + .unwrap(); + + let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); + let entered_tx = Arc::new(Mutex::new(Some(entered_tx))); + register_tool_conditional_execution_guardrail( + "pending-conditional-cross-scope", + 1, + Arc::new(move |_name, _args| { + if let Some(sender) = entered_tx.lock().unwrap().take() { + let _ = sender.send(()); + } + Box::pin(std::future::pending()) + }), + ) + .unwrap(); + + let conditional_args = json!({}); + let mut evaluation = Box::pin(tool_conditional_execution( + "cancelled-conditional-cross-scope", + &conditional_args, + )); + tokio::select! { + result = &mut evaluation => panic!("evaluation unexpectedly completed: {result:?}"), + result = entered_rx => result.unwrap(), + } + + set_thread_scope_stack(create_scope_stack()); + drop(evaluation); + flush_subscribers().unwrap(); + + let end = captured_events_snapshot(&events) + .into_iter() + .find(|event| { + event.name() == "pending-conditional-cross-scope" + && event.scope_category() == Some(ScopeCategory::End) + }) + .expect("cancelled guardrail should emit an end event"); + assert_eq!( + end.metadata(), + Some(&json!({"sanitized_by": "originating-scope"})) + ); + + set_thread_scope_stack(originating_stack); + deregister_tool_conditional_execution_guardrail("pending-conditional-cross-scope").unwrap(); + deregister_subscriber("cancelled_guardrail_cross_scope_observer").unwrap(); + pop_scope( + nemo_relay::api::scope::PopScopeParams::builder() + .handle_uuid(&owner.uuid) + .build(), + ) + .unwrap(); +} + #[tokio::test] async fn dropping_pending_llm_execution_closes_the_managed_lifecycle() { let _lock = TEST_MUTEX.lock().unwrap(); diff --git a/crates/core/tests/integration/worker_plugin_tests.rs b/crates/core/tests/integration/worker_plugin_tests.rs index b7eb75611..358447ae9 100644 --- a/crates/core/tests/integration/worker_plugin_tests.rs +++ b/crates/core/tests/integration/worker_plugin_tests.rs @@ -413,6 +413,54 @@ async fn worker_event_sanitizers_preserve_prior_field_changes() { loaded.clear(); } +#[tokio::test] +async fn worker_sanitizer_nested_publication_precedes_already_queued_event() { + let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; + let loaded = load_and_initialize_fixture(Map::new()).await; + let names = Arc::new(Mutex::new(Vec::::new())); + let captured = names.clone(); + register_subscriber( + "worker_nested_publication_order", + Arc::new(move |event| { + if event.name().starts_with("worker-nested-order-") { + captured.lock().unwrap().push(event.name().to_string()); + } + }), + ) + .expect("test subscriber should register"); + + TASK_SCOPE_STACK + .scope(create_scope_stack(), async { + event( + EmitMarkEventParams::builder() + .name("worker-nested-order-outer") + .build(), + ) + .expect("outer mark should emit"); + event( + EmitMarkEventParams::builder() + .name("worker-nested-order-later") + .build(), + ) + .expect("later mark should emit"); + }) + .await; + + flush_subscribers().expect("worker nested events should flush"); + assert_eq!( + names.lock().unwrap().as_slice(), + [ + "worker-nested-order-outer", + "worker-nested-order-inner", + "worker-nested-order-later", + ] + ); + + deregister_subscriber("worker_nested_publication_order") + .expect("test subscriber should deregister"); + loaded.clear(); +} + #[tokio::test] async fn host_cancellation_reaches_rust_worker_invocation() { let _guard = WORKER_PLUGIN_TEST_LOCK.lock().await; diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index 481f4502a..25f6b3e5e 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -1152,7 +1152,7 @@ async fn dropping_callback_future_cancels_worker_and_cleans_host_state() { ); let overlapping_scope_stack_id = callback .host_state - .insert_invocation_scope_stack(invocation_stack.clone()); + .insert_invocation_scope_stack(invocation_stack.clone(), None); let invocation_id = request.invocation_id.clone(); let callback_task = callback.clone(); let task = tokio::spawn(async move { callback_task.invoke_async(request).await }); @@ -1255,7 +1255,7 @@ fn invocation_cleanup_releases_host_state_locks_before_unwinding() { )); let stack = crate::api::runtime::create_scope_stack(); let baseline_depth = stack.read().expect("scope stack lock").scopes().len(); - let scope_stack_id = state.insert_invocation_scope_stack(stack.clone()); + let scope_stack_id = state.insert_invocation_scope_stack(stack.clone(), None); with_scope_stack(stack.clone(), || { push_scope( PushScopeParams::builder() diff --git a/crates/core/tests/unit/subscriber_dispatcher_tests.rs b/crates/core/tests/unit/subscriber_dispatcher_tests.rs index 946395ee7..bd56e7a0a 100644 --- a/crates/core/tests/unit/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/unit/subscriber_dispatcher_tests.rs @@ -3,8 +3,11 @@ use super::EventSubscriberFn; use super::native::{ DispatcherMessage, dispatcher_sender, enqueue_dispatch_message, flush_subscribers, - register_async_publication, spawn_background_publication, + register_async_publication, sanitize_event_snapshot, set_sanitizer_runtime_failure_for_test, + spawn_background_publication, }; +use crate::api::registry::RegistryRecord; +use crate::api::runtime::EventSanitizeFn; use crate::api::runtime::scope_stack::current_scope_stack; use std::sync::{Arc, Mutex, mpsc}; @@ -101,7 +104,7 @@ fn flush_does_not_wait_for_later_delivery() { .unwrap(); let (release_tx, release_rx) = tokio::sync::oneshot::channel(); - let event = serde_json::from_value(serde_json::json!({ + let event: crate::api::event::Event = serde_json::from_value(serde_json::json!({ "kind": "mark", "atof_version": "0.1", "uuid": "019c1df6-4a57-7000-8000-000000000003", @@ -274,3 +277,35 @@ fn detached_publications_share_one_background_executor_thread() { ); release_tx.send(true).unwrap(); } + +#[test] +fn sanitizer_runtime_failure_preserves_untransformed_event_snapshot() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + let event: crate::api::event::Event = serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000008", + "timestamp": "2026-07-28T00:00:00Z", + "name": "fail-open-runtime" + })) + .expect("valid event"); + let sanitizer: EventSanitizeFn = Arc::new(|_, _| { + Box::pin(async { + panic!("the unavailable sanitizer runtime must not invoke middleware"); + }) + }); + + set_sanitizer_runtime_failure_for_test(Some("injected runtime failure")); + let (published, nested) = sanitize_event_snapshot( + event.clone(), + None, + vec![RegistryRecord::new("unreachable", 0, sanitizer)], + None, + ); + set_sanitizer_runtime_failure_for_test(None); + + assert_eq!(published, Some(event)); + assert!(nested.is_empty()); +} diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index c6bb6cf86..44cbb97e1 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -4122,9 +4122,8 @@ impl AtofExporter { /// establish the delivery barrier. A stream timeout is logged and does not by itself return an /// error. #[napi] - pub fn force_flush(&self) -> napi::Result<()> { - self.inner - .force_flush() + pub fn force_flush(&self, env: Env) -> napi::Result<()> { + with_effective_scope_stack(&env, || self.inner.force_flush())? .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -4133,9 +4132,8 @@ impl AtofExporter { /// does not establish the delivery barrier. A stream timeout is logged and does not by itself /// return an error. #[napi] - pub fn shutdown(&self) -> napi::Result<()> { - self.inner - .shutdown() + pub fn shutdown(&self, env: Env) -> napi::Result<()> { + with_effective_scope_stack(&env, || self.inner.shutdown())? .map_err(|e| napi::Error::from_reason(e.to_string())) } } @@ -4210,17 +4208,15 @@ impl OpenTelemetrySubscriber { /// Force a flush of finished spans through the exporter. #[napi] - pub fn force_flush(&self) -> napi::Result<()> { - self.inner - .force_flush() + pub fn force_flush(&self, env: Env) -> napi::Result<()> { + with_effective_scope_stack(&env, || self.inner.force_flush())? .map_err(|e| napi::Error::from_reason(e.to_string())) } /// Shut down the underlying tracer provider. #[napi] - pub fn shutdown(&self) -> napi::Result<()> { - self.inner - .shutdown() + pub fn shutdown(&self, env: Env) -> napi::Result<()> { + with_effective_scope_stack(&env, || self.inner.shutdown())? .map_err(|e| napi::Error::from_reason(e.to_string())) } } diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index e74a37b5a..31a64fc84 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -2,15 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; import { describe, it } from 'node:test'; import { createRequire } from 'node:module'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; +import { promisify } from 'node:util'; const require = createRequire(import.meta.url); const lib = require('../index.js'); const plugin = require('../plugin.js'); +const execFileAsync = promisify(execFile); function capture(name) { const events = []; @@ -129,6 +132,48 @@ describe('event sanitizer registries', () => { assert.deepEqual(events.at(-1).data, { sanitized: true }); }); + it('keeps synchronous exporter flush and shutdown reentrant inside Promise sanitizers', async () => { + const addonPath = require.resolve('../index.js'); + const runExporterScenario = async (kind) => { + const script = String.raw` + const fs = require('node:fs'); + const os = require('node:os'); + const path = require('node:path'); + const lib = require(${JSON.stringify(addonPath)}); + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'nemo-relay-reentrant-exporter-')); + const exporter = ${kind === 'atof' + ? "new lib.AtofExporter({ outputDirectory: directory })" + : "new lib.OpenTelemetrySubscriber({ type: 'full', endpoint: 'http://127.0.0.1:9', timeoutMillis: 10 })"}; + const sanitizerName = ${JSON.stringify(`node-reentrant-${kind}-sanitizer`)}; + const subscriberName = ${JSON.stringify(`node-reentrant-${kind}-subscriber`)}; + lib.registerSubscriber(subscriberName, () => {}); + lib.registerMarkSanitizeGuardrail(sanitizerName, 0, async (_event, fields) => { + await new Promise((resolve) => setImmediate(resolve)); + exporter.forceFlush(); + exporter.shutdown(); + return fields; + }); + lib.event(${JSON.stringify(`node-reentrant-${kind}-event`)}); + lib.flushSubscribers().then(() => { + lib.deregisterMarkSanitizeGuardrail(sanitizerName); + lib.deregisterSubscriber(subscriberName); + fs.rmSync(directory, { recursive: true, force: true }); + process.stdout.write('ok'); + }, (error) => { + process.stderr.write(String(error?.stack ?? error)); + process.exitCode = 1; + }); + `; + const { stdout } = await execFileAsync(process.execPath, ['--eval', script], { + timeout: 10_000, + }); + assert.equal(stdout, 'ok'); + }; + + await runExporterScenario('atof'); + await runExporterScenario('otel'); + }); + it('publishes nested Promise sanitizer events before already queued events', async () => { const events = capture('node-event-sanitize-nested-order-sub'); let sanitizerEntered; diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index ca103568b..84ebb2295 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -39,6 +39,7 @@ mod py_storage; #[doc(hidden)] pub mod py_types; #[cfg(test)] +#[path = "../tests/support/mod.rs"] mod test_support; /// The `_native` PyO3 module entry point. Registers all types and functions. diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 6f4fefd7e..fb6360293 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -107,10 +107,11 @@ where if !python_event_loop_running(py)? { let result = py .detach(|| { - block_on_sync_middleware( - py_callable::PY_AWAITABLES_ALLOWED - .scope(false, TASK_SCOPE_STACK.scope(scope_stack, future)), - ) + let future = py_callable::PY_AWAITABLES_ALLOWED + .scope(false, TASK_SCOPE_STACK.scope(scope_stack, future)); + let future = with_task_publication_context(publication_context, future); + let future = with_task_nested_publication_buffer(publication_buffer, future); + block_on_sync_middleware(future) }) .map_err(to_py_err)?; return convert(py, result).map(|value| value.into_bound(py)); diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index d3881fe73..0c2aa674e 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -357,7 +357,6 @@ fn capture_python_task_locals() -> Option { struct PythonPublicationContext { task_locals: Option, context: Py, - scope_stack: nemo_relay::api::runtime::ScopeStackHandle, } pub(crate) fn capture_python_publication_context() -> Option { @@ -370,7 +369,6 @@ pub(crate) fn capture_python_publication_context() -> Option Some(Arc::new(PythonPublicationContext { task_locals: pyo3_async_runtimes::tokio::get_current_locals(py).ok(), context, - scope_stack: snapshot_scope_stack(¤t_scope_stack()).ok()?, }) as PublicationContext) }) } @@ -427,10 +425,12 @@ fn copy_publication_invocation_with_buffer<'py>( publication_buffer: Option, ) -> PyResult<(Bound<'py, PyAny>, Option)> { let invocation_context = context.context.bind(py).call_method0("copy")?; + let scope_stack = snapshot_scope_stack(¤t_scope_stack()) + .map_err(|error| PyRuntimeError::new_err(error.to_string()))?; let scope_stack = Py::new( py, PyScopeStack { - inner: context.scope_stack.clone(), + inner: scope_stack, publication_buffer, }, )?; diff --git a/crates/python/src/test_support.rs b/crates/python/tests/support/mod.rs similarity index 96% rename from crates/python/src/test_support.rs rename to crates/python/tests/support/mod.rs index 3cb62dab8..9666f009f 100644 --- a/crates/python/src/test_support.rs +++ b/crates/python/tests/support/mod.rs @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +//! Shared support for Rust tests of the Python binding. + use std::ffi::{CString, OsString}; use std::sync::{Mutex, MutexGuard, OnceLock}; @@ -86,7 +88,7 @@ pub(crate) fn init_python_test_locked(lock: MutexGuard<'static, ()>) -> PythonTe .set_item("nemo_relay", package) .expect("register test package"); let source = CString::new(include_str!( - "../../../python/nemo_relay/_event_sanitizer_context.py" + "../../../../python/nemo_relay/_event_sanitizer_context.py" )) .expect("helper source"); let filename = CString::new("_event_sanitizer_context.py").expect("helper filename"); diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 1cf889c9c..547326ac1 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -5,6 +5,7 @@ import asyncio import contextvars +import threading from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor from typing import cast @@ -130,6 +131,68 @@ async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> Even ] +def test_sync_standalone_middleware_preserves_nested_publication_order(capture_events): + _capture_name, events = capture_events + entered = threading.Event() + release = threading.Event() + + def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + if event.name == "python-sync-outer-event": + entered.set() + assert release.wait(timeout=2) + nemo_relay.tools.conditional_execution("python-nested-conditional", {}) + return fields + + guardrails.register_tool_conditional_execution( + "python-nested-conditional", + 0, + lambda _name, _args: None, + ) + guardrails.register_mark_sanitize("python-sync-nested-order", 0, sanitize) + try: + scope.event("python-sync-outer-event") + assert entered.wait(timeout=2) + scope.event("python-sync-later-event") + release.set() + subscribers.flush() + finally: + release.set() + guardrails.deregister_mark_sanitize("python-sync-nested-order") + guardrails.deregister_tool_conditional_execution("python-nested-conditional") + + assert [event.name for event in events] == [ + "python-sync-outer-event", + "python-nested-conditional", + "python-nested-conditional", + "python-sync-later-event", + ] + + +async def test_scope_start_sanitizer_uses_started_scope_context(capture_events): + _capture_name, events = capture_events + observed_scope_uuids: list[str] = [] + + async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: + if event.name == "python-start-context": + await asyncio.sleep(0) + observed_scope_uuids.append(scope.get_handle().uuid) + scope.event("python-start-context-nested") + return fields + + guardrails.register_scope_sanitize_start("python-start-context", 0, sanitize) + handle = scope.push("python-start-context", nemo_relay.ScopeType.Agent) + try: + await subscribers.flush_async() + finally: + scope.pop(handle) + await subscribers.flush_async() + guardrails.deregister_scope_sanitize_start("python-start-context") + + nested = next(event for event in events if event.name == "python-start-context-nested") + assert observed_scope_uuids == [handle.uuid] + assert nested.parent_uuid == handle.uuid + + async def test_async_mark_sanitizer_uses_each_emitter_context(capture_events): request_id = contextvars.ContextVar("request_id", default="registration") observed: dict[str, str] = {} From 372eaa1ce418d06a8c1f1531ca415ae04a699559 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 11:49:51 -0400 Subject: [PATCH 57/83] fix: harden async middleware callbacks Signed-off-by: Will Killian --- crates/cli/src/diagnostics/probes.rs | 53 +- .../cli/tests/coverage/shared/probes_tests.rs | 52 ++ crates/core/src/api/runtime.rs | 7 +- .../src/api/runtime/continuation_context.rs | 84 ++ crates/core/src/api/runtime/scope_stack.rs | 15 +- crates/core/src/api/scope.rs | 3 +- crates/core/src/observability/mod.rs | 14 +- crates/core/src/plugin/dynamic/native.rs | 523 ++++++++---- crates/core/src/plugin/dynamic/worker.rs | 117 ++- .../tests/unit/continuation_context_tests.rs | 63 ++ .../core/tests/unit/dynamic_worker_tests.rs | 87 +- crates/core/tests/unit/native_plugin_tests.rs | 808 ++++++++++++++++-- .../tests/unit/observability/mod_tests.rs | 13 + crates/ffi/nemo_relay.h | 12 + crates/ffi/src/api/mod.rs | 45 +- crates/ffi/tests/unit/api/core_tests.rs | 50 ++ crates/node/src/api/mod.rs | 71 +- crates/node/src/callable.rs | 3 +- crates/node/src/callback_factory.rs | 12 +- crates/node/src/promise_call.rs | 184 ++-- crates/node/tests/callback_error_tests.mjs | 13 + crates/node/tests/event_sanitizers_tests.mjs | 306 +------ crates/node/tests/rust/promise_call_tests.rs | 22 + crates/plugin/src/lib.rs | 15 +- crates/python/src/py_api/mod.rs | 7 +- crates/python/src/py_callable.rs | 59 +- crates/python/src/py_types/observability.rs | 14 +- .../coverage/py_callable_coverage_tests.rs | 25 + docs/about-nemo-relay/concepts/middleware.mdx | 15 + .../about-nemo-relay/concepts/subscribers.mdx | 22 +- .../dynamic-plugins/native-dynamic/about.mdx | 20 +- docs/getting-started/quick-start/nodejs.mdx | 2 +- .../adding-scopes-and-marks.mdx | 7 +- .../instrument-llm-call.mdx | 7 +- .../instrument-tool-call.mdx | 7 +- docs/reference/event-sanitizers.mdx | 42 +- docs/reference/migration-guides.mdx | 15 +- python/nemo_relay/_event_sanitizer_context.py | 8 +- python/nemo_relay/_native.pyi | 23 +- python/nemo_relay/subscribers.py | 15 +- python/tests/test_event_sanitizers.py | 133 --- 41 files changed, 2003 insertions(+), 990 deletions(-) create mode 100644 crates/cli/tests/coverage/shared/probes_tests.rs create mode 100644 crates/core/src/api/runtime/continuation_context.rs create mode 100644 crates/core/tests/unit/continuation_context_tests.rs create mode 100644 crates/core/tests/unit/observability/mod_tests.rs create mode 100644 crates/node/tests/rust/promise_call_tests.rs diff --git a/crates/cli/src/diagnostics/probes.rs b/crates/cli/src/diagnostics/probes.rs index 0fdbcd7af..88ee1f750 100644 --- a/crates/cli/src/diagnostics/probes.rs +++ b/crates/cli/src/diagnostics/probes.rs @@ -127,54 +127,5 @@ fn grpc_endpoint_port(endpoint: &reqwest::Url) -> u16 { } #[cfg(test)] -mod tcp_tests { - use super::*; - - #[tokio::test] - async fn grpc_probe_uses_tcp_connectivity() { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let endpoint = format!("http://{}", listener.local_addr().unwrap()); - let check = probe_tcp_named("OpenTelemetry endpoint", &endpoint).await; - assert_eq!(check.status, Status::Pass); - assert!(check.details.contains("gRPC TCP connection succeeded")); - } - - #[tokio::test] - async fn grpc_probe_reports_invalid_hostless_and_refused_endpoints() { - let invalid = probe_tcp_named("OpenTelemetry endpoint", "not a url").await; - assert_eq!(invalid.status, Status::Fail); - assert!(invalid.details.contains("invalid gRPC endpoint")); - - let hostless = probe_tcp_named("OpenTelemetry endpoint", "file:///tmp/collector").await; - assert_eq!(hostless.status, Status::Fail); - assert!(hostless.details.contains("has no host")); - - let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - let endpoint = format!("http://{}", listener.local_addr().unwrap()); - drop(listener); - let refused = probe_tcp_named("OpenTelemetry endpoint", &endpoint).await; - assert_eq!(refused.status, Status::Fail); - assert!( - refused.details.contains("connection failed") - || refused.details.contains("connection timed out"), - "{}", - refused.details - ); - } - - #[test] - fn grpc_probe_uses_tls_and_otlp_default_ports() { - assert_eq!( - grpc_endpoint_port(&reqwest::Url::parse("https://collector.example.com").unwrap()), - 443 - ); - assert_eq!( - grpc_endpoint_port(&reqwest::Url::parse("http://collector.example.com").unwrap()), - 4317 - ); - assert_eq!( - grpc_endpoint_port(&reqwest::Url::parse("https://collector.example.com:8443").unwrap()), - 8443 - ); - } -} +#[path = "../../tests/coverage/shared/probes_tests.rs"] +mod tcp_tests; diff --git a/crates/cli/tests/coverage/shared/probes_tests.rs b/crates/cli/tests/coverage/shared/probes_tests.rs new file mode 100644 index 000000000..17335fa38 --- /dev/null +++ b/crates/cli/tests/coverage/shared/probes_tests.rs @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; + +#[tokio::test] +async fn grpc_probe_uses_tcp_connectivity() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + let check = probe_tcp_named("OpenTelemetry endpoint", &endpoint).await; + assert_eq!(check.status, Status::Pass); + assert!(check.details.contains("gRPC TCP connection succeeded")); +} + +#[tokio::test] +async fn grpc_probe_reports_invalid_hostless_and_refused_endpoints() { + let invalid = probe_tcp_named("OpenTelemetry endpoint", "not a url").await; + assert_eq!(invalid.status, Status::Fail); + assert!(invalid.details.contains("invalid gRPC endpoint")); + + let hostless = probe_tcp_named("OpenTelemetry endpoint", "file:///tmp/collector").await; + assert_eq!(hostless.status, Status::Fail); + assert!(hostless.details.contains("has no host")); + + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + drop(listener); + let refused = probe_tcp_named("OpenTelemetry endpoint", &endpoint).await; + assert_eq!(refused.status, Status::Fail); + assert!( + refused.details.contains("connection failed") + || refused.details.contains("connection timed out"), + "{}", + refused.details + ); +} + +#[test] +fn grpc_probe_uses_tls_and_otlp_default_ports() { + assert_eq!( + grpc_endpoint_port(&reqwest::Url::parse("https://collector.example.com").unwrap()), + 443 + ); + assert_eq!( + grpc_endpoint_port(&reqwest::Url::parse("http://collector.example.com").unwrap()), + 4317 + ); + assert_eq!( + grpc_endpoint_port(&reqwest::Url::parse("https://collector.example.com:8443").unwrap()), + 8443 + ); +} diff --git a/crates/core/src/api/runtime.rs b/crates/core/src/api/runtime.rs index f27ad4fb0..1d27afb35 100644 --- a/crates/core/src/api/runtime.rs +++ b/crates/core/src/api/runtime.rs @@ -4,6 +4,7 @@ //! Advanced runtime state, callbacks, and scope-stack helpers. pub mod callbacks; +mod continuation_context; pub mod global; pub mod scope_stack; pub mod state; @@ -17,14 +18,16 @@ pub use callbacks::{ LlmStreamExecutionNextFn, LlmStreamInner, ToolConditionalFn, ToolExecutionFn, ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn, }; +#[doc(hidden)] +pub use continuation_context::MiddlewareContinuationContext; pub use global::global_context; pub use scope_stack::{ PropagationContext, ScopeStack, ScopeStackHandle, TASK_SCOPE_STACK, ThreadScopeStackBinding, capture_propagation_context, capture_propagation_context_with_root, capture_thread_scope_stack, create_scope_stack, create_scope_stack_from_propagation, current_scope_stack, propagate_scope_to_thread, restore_thread_scope_stack, scope_stack_active, - set_thread_scope_stack, snapshot_scope_stack, sync_thread_scope_stack, task_scope_push, - task_scope_remove, task_scope_top, with_active_event_uuid, with_scope_stack, + set_thread_scope_stack, sync_thread_scope_stack, task_scope_push, task_scope_remove, + task_scope_top, with_active_event_uuid, with_scope_stack, }; pub use state::NemoRelayContextState; pub use subscriber_dispatcher::flush_subscribers; diff --git a/crates/core/src/api/runtime/continuation_context.rs b/crates/core/src/api/runtime/continuation_context.rs new file mode 100644 index 000000000..7b9f0b759 --- /dev/null +++ b/crates/core/src/api/runtime/continuation_context.rs @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Internal task context captured for middleware continuations. + +use std::future::Future; + +use crate::api::optimization::{ + LlmOptimizationRecorder, current_llm_optimization_recorder, scope_llm_optimization_recorder, +}; +use crate::api::runtime::scope_stack::{ + ScopeStackHandle, TASK_SCOPE_STACK, active_event_uuid, current_scope_stack, + with_active_event_uuid, +}; +use crate::api::runtime::subscriber_dispatcher::{ + PublicationBuffer, PublicationContext, capture_nested_publication_buffer, + capture_publication_context, with_task_nested_publication_buffer, + with_task_publication_context, +}; + +/// Opaque Relay task context captured for a middleware `next` continuation. +/// +/// This is an internal cross-crate bridge for Relay's language bindings and +/// dynamic-plugin adapters. Its fields intentionally remain private. +#[doc(hidden)] +#[derive(Clone)] +pub struct MiddlewareContinuationContext { + scope_stack: ScopeStackHandle, + active_event_uuid: Option, + publication_context: Option, + publication_buffer: Option, + optimization_recorder: Option, +} + +impl MiddlewareContinuationContext { + /// Capture the Relay task context visible to the current middleware call. + #[doc(hidden)] + #[must_use] + pub fn capture() -> Self { + Self { + scope_stack: current_scope_stack(), + active_event_uuid: active_event_uuid(), + publication_context: capture_publication_context(), + publication_buffer: capture_nested_publication_buffer(), + optimization_recorder: current_llm_optimization_recorder(), + } + } + + /// Poll `future` with the captured Relay task context restored. + #[doc(hidden)] + pub async fn run(&self, future: F) -> F::Output { + let scoped = TASK_SCOPE_STACK.scope(self.scope_stack.clone(), future); + let published = with_task_publication_context(self.publication_context.clone(), scoped); + let published = + with_task_nested_publication_buffer(self.publication_buffer.clone(), published); + let active = async { + match self.active_event_uuid { + Some(uuid) => with_active_event_uuid(uuid, published).await, + None => published.await, + } + }; + match &self.optimization_recorder { + Some(recorder) => scope_llm_optimization_recorder(recorder.clone(), active).await, + None => active.await, + } + } + + /// Invoke a callback and poll its future with the captured Relay context. + /// + /// The callback itself can inspect Relay task state before constructing its + /// future, so it must be invoked only after the context is restored. + #[doc(hidden)] + pub async fn invoke(&self, callback: C) -> F::Output + where + C: FnOnce() -> F, + F: Future, + { + self.run(async move { callback().await }).await + } +} + +#[cfg(test)] +#[path = "../../../tests/unit/continuation_context_tests.rs"] +mod tests; diff --git a/crates/core/src/api/runtime/scope_stack.rs b/crates/core/src/api/runtime/scope_stack.rs index 218df680d..3bf7c03c5 100644 --- a/crates/core/src/api/runtime/scope_stack.rs +++ b/crates/core/src/api/runtime/scope_stack.rs @@ -29,7 +29,6 @@ use crate::registry::{RegistryEntry, SortedRegistry}; /// their nearest agent's freshness instead of creating a separate budget. /// Additional scopes are pushed as the public API opens lifecycle spans and /// removed when those spans close. -#[derive(Clone)] pub struct ScopeStack { stack: Vec, scope_registries: HashMap, @@ -97,6 +96,15 @@ impl PropagationContext { } impl ScopeStack { + fn snapshot(&self) -> Self { + Self { + stack: self.stack.clone(), + scope_registries: self.scope_registries.clone(), + fresh_agents: self.fresh_agents.clone(), + propagated_parent_uuid: self.propagated_parent_uuid, + } + } + /// Create a new scope stack containing only the implicit root scope. /// /// # Returns @@ -409,11 +417,11 @@ pub fn create_scope_stack() -> ScopeStackHandle { /// Clone a scope stack into an isolated emission-time snapshot. #[doc(hidden)] -pub fn snapshot_scope_stack(handle: &ScopeStackHandle) -> Result { +pub(crate) fn snapshot_scope_stack(handle: &ScopeStackHandle) -> Result { let stack = handle .read() .unwrap_or_else(|error| error.into_inner()) - .clone(); + .snapshot(); Ok(Arc::new(RwLock::new(stack))) } @@ -461,7 +469,6 @@ pub async fn with_active_event_uuid(uuid: Uuid, future: impl Future Option { ACTIVE_EVENT_UUID.try_with(|uuid| *uuid).ok() } diff --git a/crates/core/src/api/scope.rs b/crates/core/src/api/scope.rs index 2e3dbd49a..03ecdf261 100644 --- a/crates/core/src/api/scope.rs +++ b/crates/core/src/api/scope.rs @@ -3,9 +3,10 @@ use crate::api::event::{BaseEvent, CategoryProfile, DataSchema, EventCategory, MarkEvent}; use crate::api::runtime::global_context; +use crate::api::runtime::scope_stack::snapshot_scope_stack; use crate::api::runtime::subscriber_dispatcher; use crate::api::runtime::{ - current_scope_stack, snapshot_scope_stack, task_scope_push, task_scope_remove, task_scope_top, + current_scope_stack, task_scope_push, task_scope_remove, task_scope_top, }; use crate::api::shared::{ ensure_runtime_owner, resolve_parent_uuid, snapshot_event_sanitizers, diff --git a/crates/core/src/observability/mod.rs b/crates/core/src/observability/mod.rs index 287aab6ea..55f29400c 100644 --- a/crates/core/src/observability/mod.rs +++ b/crates/core/src/observability/mod.rs @@ -583,15 +583,5 @@ where mod attribute_projection_tests; #[cfg(test)] -mod tests { - use super::{relay_span_id, relay_trace_id}; - use uuid::Uuid; - - #[test] - fn relay_id_conversions_preserve_zero_bytes() { - let uuid = Uuid::nil(); - - assert_eq!(relay_trace_id(uuid).to_bytes(), [0; 16]); - assert_eq!(relay_span_id(uuid).to_bytes(), [0; 8]); - } -} +#[path = "../../tests/unit/observability/mod_tests.rs"] +mod tests; diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 07f3dd2ac..88d9d4650 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -18,6 +18,35 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use std::task::{Context, Poll}; +use futures_util::FutureExt; + +use crate::api::event::{Event, EventSanitizeFields}; +use crate::api::llm::{LlmRequest, LlmRequestInterceptOutcome}; +#[cfg(test)] +use crate::api::runtime::current_scope_stack; +use crate::api::runtime::{ + EventSanitizeFn, EventSubscriberFn, LlmCodecIdentity, LlmConditionalFn, LlmExecutionFn, + LlmExecutionNextFn, LlmJsonStream, LlmRequestInterceptFn, LlmSanitizeRequestContext, + LlmSanitizeRequestFn, LlmSanitizeResponseContext, LlmSanitizeResponseFn, LlmStreamExecutionFn, + LlmStreamExecutionNextFn, MiddlewareContinuationContext, ToolConditionalFn, ToolExecutionFn, + ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn, +}; +use crate::api::runtime::{ + ScopeStackHandle, ThreadScopeStackBinding, capture_thread_scope_stack, create_scope_stack, + restore_thread_scope_stack, scope_stack_active, set_thread_scope_stack, with_scope_stack, +}; +use crate::api::scope::{ + EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeHandle, ScopeType, +}; +use crate::api::scope::{event as emit_scope_mark, get_handle, pop_scope, push_scope}; +use crate::api::tool::ToolExecutionInterceptOutcome; +use crate::codec::request::AnnotatedLlmRequest; +use crate::codec::traits::{LlmCodec, LlmResponseCodec}; +use crate::error::{FlowError, Result as FlowResult}; +use crate::plugin::{ + ConfigDiagnostic, DiagnosticLevel, Plugin, PluginError, PluginRegistrationContext, + deregister_plugin_registration_checked, register_plugin_tracked, +}; use chrono::{DateTime, Utc}; use libloading::{Library, Symbol}; use nemo_relay_plugin::{ @@ -44,33 +73,6 @@ use sha2::{Digest, Sha256}; use tokio::runtime::Runtime; use tokio_stream::{Stream, StreamExt}; -use crate::api::event::{Event, EventSanitizeFields}; -use crate::api::llm::{LlmRequest, LlmRequestInterceptOutcome}; -use crate::api::runtime::{ - EventSanitizeFn, EventSubscriberFn, LlmCodecIdentity, LlmConditionalFn, LlmExecutionFn, - LlmExecutionNextFn, LlmJsonStream, LlmRequestInterceptFn, LlmSanitizeRequestContext, - LlmSanitizeRequestFn, LlmSanitizeResponseContext, LlmSanitizeResponseFn, LlmStreamExecutionFn, - LlmStreamExecutionNextFn, ToolConditionalFn, ToolExecutionFn, ToolExecutionNextFn, - ToolInterceptFn, ToolSanitizeFn, -}; -use crate::api::runtime::{ - ScopeStackHandle, TASK_SCOPE_STACK, ThreadScopeStackBinding, capture_thread_scope_stack, - create_scope_stack, current_scope_stack, restore_thread_scope_stack, scope_stack_active, - set_thread_scope_stack, with_scope_stack, -}; -use crate::api::scope::{ - EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeHandle, ScopeType, -}; -use crate::api::scope::{event as emit_scope_mark, get_handle, pop_scope, push_scope}; -use crate::api::tool::ToolExecutionInterceptOutcome; -use crate::codec::request::AnnotatedLlmRequest; -use crate::codec::traits::{LlmCodec, LlmResponseCodec}; -use crate::error::{FlowError, Result as FlowResult}; -use crate::plugin::{ - ConfigDiagnostic, DiagnosticLevel, Plugin, PluginError, PluginRegistrationContext, - deregister_plugin_registration_checked, register_plugin_tracked, -}; - use super::{ DynamicPluginKind, DynamicPluginManifest, DynamicPluginManifestLoad, DynamicPluginTeardownOutcome, deregister_tracked_registrations_checked, @@ -1295,6 +1297,14 @@ fn status_from_flow_error(err: FlowError) -> NemoRelayStatus { } } +fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> &str { + payload + .downcast_ref::<&str>() + .copied() + .or_else(|| payload.downcast_ref::().map(String::as_str)) + .unwrap_or("unknown panic payload") +} + fn native_runtime() -> &'static Runtime { static RUNTIME: OnceLock = OnceLock::new(); RUNTIME.get_or_init(|| { @@ -1305,19 +1315,19 @@ fn native_runtime() -> &'static Runtime { }) } -fn spawn_with_current_scope(f: impl FnOnce() -> T + Send + 'static) -> std::thread::JoinHandle +fn spawn_with_continuation_context( + context: MiddlewareContinuationContext, + callback: C, +) -> std::thread::JoinHandle where + C: FnOnce() -> F + Send + 'static, + F: Future + Send + 'static, T: Send + 'static, { let binding = capture_thread_scope_stack(); - let visible_stack = scope_stack_active().then(current_scope_stack); std::thread::spawn(move || { restore_thread_scope_stack(binding); - if let Some(stack) = visible_stack { - with_scope_stack(stack, f) - } else { - f() - } + native_runtime().block_on(context.invoke(callback)) }) } @@ -1388,6 +1398,8 @@ struct NativeAsyncCompletion { cancelled: AtomicBool, next_invoked: AtomicBool, next_abort: Mutex>, + #[cfg(test)] + before_settlement_lock: Option>, // A pending native callback can continue running after its completion // wakes the awaiting task. Keep the callback's dynamic-library instance // alive until native code explicitly releases this handle. @@ -1397,18 +1409,31 @@ struct NativeAsyncCompletion { struct NativeAsyncWait { completion: Arc, receiver: tokio::sync::oneshot::Receiver>, + completed: bool, +} + +impl NativeAsyncWait { + async fn receive(&mut self) -> FlowResult { + let result = (&mut self.receiver).await.map_err(|_| { + FlowError::Internal("native async callback dropped without settling".into()) + })?; + self.completed = true; + result + } } impl Drop for NativeAsyncWait { fn drop(&mut self) { - self.completion.cancelled.store(true, Ordering::Release); - if let Some(abort) = self + if self.completed { + return; + } + let mut next_abort = self .completion .next_abort .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - { + .unwrap_or_else(|error| error.into_inner()); + self.completion.cancelled.store(true, Ordering::Release); + if let Some(abort) = next_abort.take() { abort.abort(); } } @@ -1423,17 +1448,35 @@ enum NativeAsyncNextInner { struct NativeAsyncNext { inner: NativeAsyncNextInner, runtime: tokio::runtime::Handle, - scope_stack: ScopeStackHandle, + context: MiddlewareContinuationContext, // The native callback owns this handle independently of its completion. // Retaining the library here prevents an unload while it still uses `next`. _callback_user_data: Option>, } +impl NativeAsyncNext { + fn new( + inner: NativeAsyncNextInner, + runtime: tokio::runtime::Handle, + callback_user_data: Option>, + ) -> Self { + Self { + inner, + runtime, + context: MiddlewareContinuationContext::capture(), + _callback_user_data: callback_user_data, + } + } +} + struct NativeAsyncStream { sender: Mutex>>>, cancelled: AtomicBool, next_invoked: AtomicBool, downstream_abort: Mutex>, + settlement: Mutex<()>, + #[cfg(test)] + before_settlement_lock: Option>, _callback_user_data: Option>, } @@ -1453,6 +1496,19 @@ impl NativeAsyncStreamCallbackGuard { fn finish(&mut self) { self.active = false; } + + fn fail(&mut self, error: &str) { + if self.active + && !self.stream.cancelled.load(Ordering::Acquire) + && let Some(message) = native_string_from_str(error) + { + unsafe { + let _ = (self.cb)(self.user_data as *mut c_void, ptr::null(), message, false); + native_string_free(message); + } + } + self.active = false; + } } impl Drop for NativeAsyncStreamCallbackGuard { @@ -1480,21 +1536,26 @@ impl Stream for NativeAsyncStreamReceiver { impl Drop for NativeAsyncStreamReceiver { fn drop(&mut self) { + let mut downstream_abort = self + .stream + .downstream_abort + .lock() + .unwrap_or_else(|error| error.into_inner()); + let _settlement = self + .stream + .settlement + .lock() + .unwrap_or_else(|error| error.into_inner()); self.stream.cancelled.store(true, Ordering::Release); + if let Some(abort) = downstream_abort.take() { + abort.abort(); + } + drop(downstream_abort); self.stream .sender .lock() .unwrap_or_else(|error| error.into_inner()) .take(); - if let Some(abort) = self - .stream - .downstream_abort - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - { - abort.abort(); - } } } @@ -1522,16 +1583,17 @@ async fn invoke_native_async_callback( cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + #[cfg(test)] + before_settlement_lock: None, _callback_user_data: Some(user_data.clone()), }); let completion_ref = Arc::into_raw(completion.clone()) as usize; let next_ref = match (next, runtime) { - (Some(inner), Some(runtime)) => Some(Arc::into_raw(Arc::new(NativeAsyncNext { + (Some(inner), Some(runtime)) => Some(Arc::into_raw(Arc::new(NativeAsyncNext::new( inner, runtime, - scope_stack: current_scope_stack(), - _callback_user_data: Some(user_data.clone()), - })) as usize), + Some(user_data.clone()), + ))) as usize), (None, None) => None, _ => unreachable!("runtime is present exactly for native async intercepts"), }; @@ -1590,10 +1652,9 @@ async fn invoke_native_async_callback( let mut wait = NativeAsyncWait { completion, receiver, + completed: false, }; - (&mut wait.receiver) - .await - .map_err(|_| FlowError::Internal("native async callback dropped without settling".into()))? + wait.receive().await } unsafe extern "C" fn native_async_completion_resolve_json( @@ -1604,19 +1665,22 @@ unsafe extern "C" fn native_async_completion_resolve_json( else { return NemoRelayStatus::NullPointer; }; - if completion.cancelled.load(Ordering::Acquire) { - return NemoRelayStatus::InvalidArg; - } let value = match parse_json_arg(value_json, "native async completion result") { Ok(value) => value, Err(status) => return status, }; - if let Some(abort) = completion + #[cfg(test)] + if let Some(barrier) = &completion.before_settlement_lock { + barrier.wait(); + } + let mut next_abort = completion .next_abort .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - { + .unwrap_or_else(|error| error.into_inner()); + if completion.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } + if let Some(abort) = next_abort.take() { abort.abort(); } let Some(sender) = completion @@ -1639,9 +1703,6 @@ unsafe extern "C" fn native_async_completion_reject( else { return NemoRelayStatus::NullPointer; }; - if completion.cancelled.load(Ordering::Acquire) { - return NemoRelayStatus::InvalidArg; - } let message = if message.is_null() { "native async callback rejected".to_string() } else { @@ -1653,12 +1714,18 @@ unsafe extern "C" fn native_async_completion_reject( } } }; - if let Some(abort) = completion + #[cfg(test)] + if let Some(barrier) = &completion.before_settlement_lock { + barrier.wait(); + } + let mut next_abort = completion .next_abort .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - { + .unwrap_or_else(|error| error.into_inner()); + if completion.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } + if let Some(abort) = next_abort.take() { abort.abort(); } let Some(sender) = completion @@ -1709,6 +1776,17 @@ unsafe extern "C" fn native_async_stream_push_json( Ok(chunk) => chunk, Err(status) => return status, }; + #[cfg(test)] + if let Some(barrier) = &stream.before_settlement_lock { + barrier.wait(); + } + let _settlement = stream + .settlement + .lock() + .unwrap_or_else(|error| error.into_inner()); + if stream.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } let sender = stream .sender .lock() @@ -1735,6 +1813,17 @@ unsafe extern "C" fn native_async_stream_finish( let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else { return NemoRelayStatus::NullPointer; }; + #[cfg(test)] + if let Some(barrier) = &stream.before_settlement_lock { + barrier.wait(); + } + let _settlement = stream + .settlement + .lock() + .unwrap_or_else(|error| error.into_inner()); + if stream.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } if stream .sender .lock() @@ -1758,6 +1847,17 @@ unsafe extern "C" fn native_async_stream_reject( }; let message = read_native_string(message).unwrap_or_else(|_| "native async stream rejected".to_string()); + #[cfg(test)] + if let Some(barrier) = &stream.before_settlement_lock { + barrier.wait(); + } + let _settlement = stream + .settlement + .lock() + .unwrap_or_else(|error| error.into_inner()); + if stream.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } let mut sender_guard = stream .sender .lock() @@ -1856,11 +1956,14 @@ unsafe extern "C" fn native_async_next_invoke( } _ => unreachable!("native next invocation kind matched its continuation"), }; - let scope_stack = next.scope_stack.clone(); + let continuation_context = next.context.clone(); let mut abort_guard = completion .next_abort .lock() .unwrap_or_else(|error| error.into_inner()); + if completion.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } let unsettled = completion .sender .lock() @@ -1870,26 +1973,43 @@ unsafe extern "C" fn native_async_next_invoke( set_native_last_error("native async next was already invoked for this completion"); return NemoRelayStatus::InvalidArg; } + let (start_tx, start_rx) = tokio::sync::oneshot::channel(); let completion_for_task = Arc::clone(&completion); - let task = next - .runtime - .spawn(TASK_SCOPE_STACK.scope(scope_stack, async move { - let result = future.await; - completion_for_task - .next_abort - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take(); - if let Some(sender) = completion_for_task - .sender - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - { - let _ = sender.send(result); - } - })); - *abort_guard = Some(task.abort_handle()); + let task = next.runtime.spawn(async move { + if start_rx.await.is_err() { + return; + } + let result = AssertUnwindSafe(continuation_context.run(future)) + .catch_unwind() + .await; + let result = result.unwrap_or_else(|payload| { + Err(FlowError::Internal(format!( + "native async next continuation panicked: {}", + panic_payload_message(payload.as_ref()) + ))) + }); + completion_for_task + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take(); + if let Some(sender) = completion_for_task + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + let _ = sender.send(result); + } + }); + let abort = task.abort_handle(); + *abort_guard = Some(abort.clone()); + if completion.cancelled.load(Ordering::Acquire) { + abort_guard.take(); + abort.abort(); + return NemoRelayStatus::InvalidArg; + } + let _ = start_tx.send(()); NemoRelayStatus::Ok } @@ -1921,6 +2041,13 @@ unsafe extern "C" fn native_async_next_invoke_stream( Ok(request) => request, Err(status) => return status, }; + let mut downstream_abort = output_stream + .downstream_abort + .lock() + .unwrap_or_else(|error| error.into_inner()); + if output_stream.cancelled.load(Ordering::Acquire) { + return NemoRelayStatus::InvalidArg; + } if output_stream.next_invoked.swap(true, Ordering::AcqRel) { set_native_last_error( "native async stream next was already invoked for this output stream", @@ -1928,83 +2055,106 @@ unsafe extern "C" fn native_async_next_invoke_stream( return NemoRelayStatus::InvalidArg; } let next_fn = next_fn.clone(); - let scope_stack = next.scope_stack.clone(); + let continuation_context = next.context.clone(); let library_guard = next._callback_user_data.clone(); let user_data = user_data as usize; let output_stream_for_task = Arc::clone(&output_stream); - let task = next - .runtime - .spawn(TASK_SCOPE_STACK.scope(scope_stack, async move { - let _library_guard = library_guard; - let mut callback_guard = NativeAsyncStreamCallbackGuard { - cb, - user_data, - stream: output_stream_for_task, - active: true, - }; - match next_fn(request).await { - Ok(mut stream) => { - while let Some(item) = stream.next().await { - match item { - Ok(chunk) => { - if let Some(chunk) = native_string_from_json(&chunk) { - let keep_going = unsafe { - cb(user_data as *mut c_void, chunk, ptr::null(), false) - }; - unsafe { - native_string_free(chunk); + let (start_tx, start_rx) = tokio::sync::oneshot::channel(); + let task = next.runtime.spawn(async move { + if start_rx.await.is_err() { + return; + } + continuation_context + .run(async move { + let _library_guard = library_guard; + let mut callback_guard = NativeAsyncStreamCallbackGuard { + cb, + user_data, + stream: output_stream_for_task, + active: true, + }; + let result = AssertUnwindSafe(async { + match next_fn(request).await { + Ok(mut stream) => { + while let Some(item) = stream.next().await { + match item { + Ok(chunk) => { + if let Some(chunk) = native_string_from_json(&chunk) { + let keep_going = unsafe { + cb( + user_data as *mut c_void, + chunk, + ptr::null(), + false, + ) + }; + unsafe { + native_string_free(chunk); + } + if !keep_going { + callback_guard.finish(); + return; + } + } else { + break; + } } - if !keep_going { - callback_guard.finish(); + Err(error) => { + if let Some(message) = + native_string_from_str(&error.to_string()) + { + unsafe { + let _ = cb( + user_data as *mut c_void, + ptr::null(), + message, + false, + ); + native_string_free(message); + } + callback_guard.finish(); + } return; } - } else { - break; } } - Err(error) => { - if let Some(message) = native_string_from_str(&error.to_string()) { - unsafe { - let _ = cb( - user_data as *mut c_void, - ptr::null(), - message, - false, - ); - native_string_free(message); - } - callback_guard.finish(); - } - return; + unsafe { + let _ = + cb(user_data as *mut c_void, ptr::null(), ptr::null(), true); } + callback_guard.finish(); } - } - unsafe { - let _ = cb(user_data as *mut c_void, ptr::null(), ptr::null(), true); - } - callback_guard.finish(); - } - Err(error) => { - if let Some(message) = native_string_from_str(&error.to_string()) { - unsafe { - let _ = cb(user_data as *mut c_void, ptr::null(), message, false); - native_string_free(message); + Err(error) => { + if let Some(message) = native_string_from_str(&error.to_string()) { + unsafe { + let _ = + cb(user_data as *mut c_void, ptr::null(), message, false); + native_string_free(message); + } + callback_guard.finish(); + } } - callback_guard.finish(); } + }) + .catch_unwind() + .await; + if let Err(payload) = result { + callback_guard.fail(&format!( + "native async stream continuation panicked: {}", + panic_payload_message(payload.as_ref()) + )); } - } - })); + }) + .await; + }); let abort = task.abort_handle(); - let mut downstream_abort = output_stream - .downstream_abort - .lock() - .unwrap_or_else(|error| error.into_inner()); + *downstream_abort = Some(abort.clone()); if output_stream.cancelled.load(Ordering::Acquire) { + downstream_abort.take(); abort.abort(); - } else { - *downstream_abort = Some(abort); + return NemoRelayStatus::InvalidArg; } + let _ = start_tx.send(()); NemoRelayStatus::Ok } @@ -2285,12 +2435,11 @@ fn wrap_native_incremental_llm_stream_execution( "native async stream intercept requires a Tokio runtime: {error}" )) })?; - let next_ref = Arc::into_raw(Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::LlmStream(next), + let next_ref = Arc::into_raw(Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(next), runtime, - scope_stack: current_scope_stack(), - _callback_user_data: Some(user_data.clone()), - })); + Some(user_data.clone()), + ))); let (sender, receiver) = tokio::sync::mpsc::channel(NATIVE_ASYNC_STREAM_CHANNEL_CAPACITY); let stream = Arc::new(NativeAsyncStream { @@ -2298,6 +2447,9 @@ fn wrap_native_incremental_llm_stream_execution( cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + #[cfg(test)] + before_settlement_lock: None, _callback_user_data: Some(user_data.clone()), }); let stream_ref = Arc::into_raw(stream.clone()); @@ -3158,7 +3310,8 @@ unsafe extern "C" fn native_tool_next( Err(status) => return status, }; let next = unsafe { (*(next_ctx as *const ToolExecutionNextFn)).clone() }; - let result = spawn_with_current_scope(move || native_runtime().block_on(next(args))).join(); + let context = MiddlewareContinuationContext::capture(); + let result = spawn_with_continuation_context(context, move || next(args)).join(); match result { Ok(Ok(result)) => write_native_json(&result, out_json), Ok(Err(err)) => status_from_flow_error(err), @@ -3506,7 +3659,8 @@ unsafe extern "C" fn native_llm_next( Err(status) => return status, }; let next = unsafe { (*(next_ctx as *const LlmExecutionNextFn)).clone() }; - let result = spawn_with_current_scope(move || native_runtime().block_on(next(request))).join(); + let context = MiddlewareContinuationContext::capture(); + let result = spawn_with_continuation_context(context, move || next(request)).join(); match result { Ok(Ok(result)) => write_native_json(&result, out_json), Ok(Err(err)) => status_from_flow_error(err), @@ -3587,10 +3741,14 @@ unsafe extern "C" fn native_llm_stream_next( Err(status) => return status, }; let next = unsafe { (*(next_ctx as *const LlmStreamExecutionNextFn)).clone() }; - let result = spawn_with_current_scope(move || native_runtime().block_on(next(request))).join(); + let context = MiddlewareContinuationContext::capture(); + let stream_context = context.clone(); + let result = spawn_with_continuation_context(context, move || next(request)).join(); match result { Ok(Ok(stream)) => { - unsafe { *out_stream = relay_stream_to_native_stream(stream) }; + unsafe { + *out_stream = relay_stream_to_native_stream_with_context(stream, stream_context) + }; NemoRelayStatus::Ok } Ok(Err(err)) => status_from_flow_error(err), @@ -3729,6 +3887,7 @@ fn drop_native_stream(mut raw: NemoRelayNativeLlmStreamV1) { struct NativeHostLlmStream { stream: Arc>>, + context: MiddlewareContinuationContext, } struct NativeStreamNextContext { @@ -3752,9 +3911,18 @@ impl Drop for NativeStreamNextContext { } } +#[cfg(test)] fn relay_stream_to_native_stream(stream: LlmJsonStream) -> NemoRelayNativeLlmStreamV1 { + relay_stream_to_native_stream_with_context(stream, MiddlewareContinuationContext::capture()) +} + +fn relay_stream_to_native_stream_with_context( + stream: LlmJsonStream, + context: MiddlewareContinuationContext, +) -> NemoRelayNativeLlmStreamV1 { let state = Box::new(NativeHostLlmStream { stream: Arc::new(Mutex::new(Some(stream))), + context, }); NemoRelayNativeLlmStreamV1 { struct_size: std::mem::size_of::(), @@ -3776,26 +3944,25 @@ unsafe extern "C" fn poll_relay_llm_stream( unsafe { *out_json = ptr::null_mut() }; let state = unsafe { &*(user_data as *const NativeHostLlmStream) }; let stream = state.stream.clone(); - let result = spawn_with_current_scope(move || { - native_runtime().block_on(async move { - let Some(mut current) = stream - .lock() - .map_err(|_| FlowError::Internal("native host LLM stream lock poisoned".into()))? - .take() - else { - return Ok(None); - }; - match current.next().await { - Some(Ok(chunk)) => { - *stream.lock().map_err(|_| { - FlowError::Internal("native host LLM stream lock poisoned".into()) - })? = Some(current); - Ok(Some(chunk)) - } - Some(Err(err)) => Err(err), - None => Ok(None), + let context = state.context.clone(); + let result = spawn_with_continuation_context(context, move || async move { + let Some(mut current) = stream + .lock() + .map_err(|_| FlowError::Internal("native host LLM stream lock poisoned".into()))? + .take() + else { + return Ok(None); + }; + match current.next().await { + Some(Ok(chunk)) => { + *stream.lock().map_err(|_| { + FlowError::Internal("native host LLM stream lock poisoned".into()) + })? = Some(current); + Ok(Some(chunk)) } - }) + Some(Err(err)) => Err(err), + None => Ok(None), + } }) .join(); match result { diff --git a/crates/core/src/plugin/dynamic/worker.rs b/crates/core/src/plugin/dynamic/worker.rs index b11bb431d..e6e041fed 100644 --- a/crates/core/src/plugin/dynamic/worker.rs +++ b/crates/core/src/plugin/dynamic/worker.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use std::future::Future; +use std::panic::AssertUnwindSafe; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::process::{Child, Command, Stdio}; @@ -12,6 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; +use futures_util::FutureExt; use nemo_relay_worker_proto::v1::plugin_worker_client::PluginWorkerClient; use nemo_relay_worker_proto::v1::relay_host_runtime_server::{ RelayHostRuntime, RelayHostRuntimeServer, @@ -58,20 +60,13 @@ use tower::service_fn; use crate::api::event::{Event, EventSanitizeFields}; use crate::api::llm::{LLM_REQUEST_INTERCEPT_OUTCOME_SCHEMA, LlmRequest}; -use crate::api::optimization::{ - LlmOptimizationRecorder, current_llm_optimization_recorder, scope_llm_optimization_recorder, -}; -use crate::api::runtime::scope_stack::active_event_uuid; use crate::api::runtime::subscriber_dispatcher::{ - PublicationBuffer, PublicationContext, capture_nested_publication_buffer, - capture_publication_context, with_nested_publication_buffer, - with_task_nested_publication_buffer, with_task_publication_context, + PublicationBuffer, capture_nested_publication_buffer, with_nested_publication_buffer, }; use crate::api::runtime::{ EventSanitizeFn, LlmCodecIdentity, LlmExecutionNextFn, LlmJsonStream, LlmSanitizeRequestContext, LlmSanitizeResponseContext, LlmStreamExecutionNextFn, - TASK_SCOPE_STACK, ToolExecutionNextFn, current_scope_stack, with_active_event_uuid, - with_scope_stack, + MiddlewareContinuationContext, ToolExecutionNextFn, current_scope_stack, with_scope_stack, }; use crate::api::scope::{ EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeHandle, ScopeType, @@ -2429,57 +2424,19 @@ struct StoredInvocationContext { publication_buffer: Option, } -#[derive(Clone)] -struct ContinuationContext { - scope_stack: crate::api::runtime::ScopeStackHandle, - active_event_uuid: Option, - publication_context: Option, - publication_buffer: Option, - optimization_recorder: Option, -} - -impl ContinuationContext { - fn capture() -> Self { - Self { - scope_stack: current_scope_stack(), - active_event_uuid: active_event_uuid(), - publication_context: capture_publication_context(), - publication_buffer: capture_nested_publication_buffer(), - optimization_recorder: current_llm_optimization_recorder(), - } - } - - async fn run(&self, future: F) -> F::Output { - let scoped = TASK_SCOPE_STACK.scope(self.scope_stack.clone(), future); - let published = with_task_publication_context(self.publication_context.clone(), scoped); - let published = - with_task_nested_publication_buffer(self.publication_buffer.clone(), published); - let active = async { - match self.active_event_uuid { - Some(uuid) => with_active_event_uuid(uuid, published).await, - None => published.await, - } - }; - match &self.optimization_recorder { - Some(recorder) => scope_llm_optimization_recorder(recorder.clone(), active).await, - None => active.await, - } - } -} - #[derive(Clone)] enum Continuation { Tool { next: ToolExecutionNextFn, - context: ContinuationContext, + context: MiddlewareContinuationContext, }, Llm { next: LlmExecutionNextFn, - context: ContinuationContext, + context: MiddlewareContinuationContext, }, LlmStream { next: LlmStreamExecutionNextFn, - context: ContinuationContext, + context: MiddlewareContinuationContext, }, } @@ -2487,21 +2444,21 @@ impl Continuation { fn tool(next: ToolExecutionNextFn) -> Self { Self::Tool { next, - context: ContinuationContext::capture(), + context: MiddlewareContinuationContext::capture(), } } fn llm(next: LlmExecutionNextFn) -> Self { Self::Llm { next, - context: ContinuationContext::capture(), + context: MiddlewareContinuationContext::capture(), } } fn llm_stream(next: LlmStreamExecutionNextFn) -> Self { Self::LlmStream { next, - context: ContinuationContext::capture(), + context: MiddlewareContinuationContext::capture(), } } } @@ -2679,7 +2636,15 @@ impl RelayHostRuntime for WorkerHostRuntimeService { required_envelope(request.value, "tool next value").map_err(status_from_flow)?; let value = decode_json_envelope::(&value) .map_err(|err| Status::invalid_argument(format!("invalid tool next JSON: {err}")))?; - let result = context.run(next(value)).await; + let result = AssertUnwindSafe(context.invoke(move || next(value))) + .catch_unwind() + .await + .unwrap_or_else(|payload| { + Err(FlowError::Internal(format!( + "worker tool continuation panicked: {}", + panic_payload_message(payload.as_ref()) + ))) + }); Ok(Response::new(json_result(result))) } @@ -2700,7 +2665,15 @@ impl RelayHostRuntime for WorkerHostRuntimeService { required_envelope(request.request, "llm next request").map_err(status_from_flow)?; let request = decode_json_envelope::(&request) .map_err(|err| Status::invalid_argument(format!("invalid LLM next request: {err}")))?; - let result = context.run(next(request)).await; + let result = AssertUnwindSafe(context.invoke(move || next(request))) + .catch_unwind() + .await + .unwrap_or_else(|payload| { + Err(FlowError::Internal(format!( + "worker LLM continuation panicked: {}", + panic_payload_message(payload.as_ref()) + ))) + }); Ok(Response::new(json_result(result))) } @@ -2725,7 +2698,16 @@ impl RelayHostRuntime for WorkerHostRuntimeService { let request = decode_json_envelope::(&request).map_err(|err| { Status::invalid_argument(format!("invalid LLM stream next request: {err}")) })?; - let stream = context.run(next(request)).await.map_err(status_from_flow)?; + let stream = AssertUnwindSafe(context.invoke(move || next(request))) + .catch_unwind() + .await + .map_err(|payload| { + Status::internal(format!( + "worker stream continuation panicked: {}", + panic_payload_message(payload.as_ref()) + )) + })? + .map_err(status_from_flow)?; let (tx, rx) = mpsc::channel(16); tokio::spawn(async move { context @@ -2735,12 +2717,23 @@ impl RelayHostRuntime for WorkerHostRuntimeService { tokio::select! { biased; _ = tx.closed() => break, - item = stream.next() => { - let Some(item) = item else { - break; - }; - if tx.send(item).await.is_err() { - break; + item = AssertUnwindSafe(stream.next()).catch_unwind() => { + match item { + Ok(Some(item)) => { + if tx.send(item).await.is_err() { + break; + } + } + Ok(None) => break, + Err(payload) => { + let _ = tx + .send(Err(FlowError::Internal(format!( + "worker stream continuation panicked: {}", + panic_payload_message(payload.as_ref()) + )))) + .await; + break; + } } } } diff --git a/crates/core/tests/unit/continuation_context_tests.rs b/crates/core/tests/unit/continuation_context_tests.rs new file mode 100644 index 000000000..b9a11355d --- /dev/null +++ b/crates/core/tests/unit/continuation_context_tests.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::api::optimization::{ + LlmOptimizationRecorder, record_llm_optimization_contribution, scope_llm_optimization_recorder, +}; +use crate::api::runtime::scope_stack::{ + TASK_SCOPE_STACK, active_event_uuid, create_scope_stack, current_scope_stack, + with_active_event_uuid, +}; +use crate::codec::optimization::LlmOptimizationContribution; +use std::sync::Arc; + +#[test] +fn continuation_context_restores_all_managed_execution_state() { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + let scope_stack = create_scope_stack(); + let event_uuid = uuid::Uuid::now_v7(); + let recorder = LlmOptimizationRecorder::default(); + let context = TASK_SCOPE_STACK + .scope( + scope_stack.clone(), + with_active_event_uuid( + event_uuid, + scope_llm_optimization_recorder(recorder, async { + MiddlewareContinuationContext::capture() + }), + ), + ) + .await; + + let observed = tokio::spawn(async move { + context + .invoke(move || { + let prelude_event_uuid = active_event_uuid(); + let prelude_scope_stack = current_scope_stack(); + async move { + let recorded = record_llm_optimization_contribution( + LlmOptimizationContribution::new("test.continuation", "context"), + ); + ( + prelude_event_uuid, + prelude_scope_stack, + active_event_uuid(), + recorded, + current_scope_stack(), + ) + } + }) + .await + }) + .await + .unwrap(); + + assert_eq!(observed.0, Some(event_uuid)); + assert!(Arc::ptr_eq(&observed.1, &scope_stack)); + assert_eq!(observed.2, Some(event_uuid)); + assert!(observed.3); + assert!(Arc::ptr_eq(&observed.4, &scope_stack)); + }); +} diff --git a/crates/core/tests/unit/dynamic_worker_tests.rs b/crates/core/tests/unit/dynamic_worker_tests.rs index 25f6b3e5e..01a4f9b1d 100644 --- a/crates/core/tests/unit/dynamic_worker_tests.rs +++ b/crates/core/tests/unit/dynamic_worker_tests.rs @@ -47,7 +47,7 @@ async fn continuation_context_preserves_optimization_recorder_across_tasks() { for producer in ["worker-unary-next", "worker-stream-next"] { let recorder = LlmOptimizationRecorder::default(); let context = scope_llm_optimization_recorder(recorder.clone(), async { - ContinuationContext::capture() + MiddlewareContinuationContext::capture() }) .await; tokio::spawn(async move { @@ -2038,6 +2038,29 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { .expect_err("invalid tool next JSON should fail"); assert_eq!(invalid_tool_json.code(), tonic::Code::InvalidArgument); + let tool_continuation = state + .insert_continuation(Continuation::tool(Arc::new(|_value| { + Box::pin(async move { + panic!("worker tool next panic"); + }) + }))) + .expect("panicking tool continuation should insert"); + let result = service + .tool_next(Request::new(ToolNextRequest { + activation_id: ACTIVATION_ID.into(), + auth_token: AUTH_TOKEN.into(), + continuation_id: tool_continuation, + value: Some(json_envelope(JSON_SCHEMA, &json!({})).expect("json envelope")), + })) + .await + .expect("tool panic should become a structured result") + .into_inner(); + assert!( + result + .error + .is_some_and(|error| error.message.contains("worker tool next panic")) + ); + let llm_continuation = state .insert_continuation(Continuation::llm(Arc::new(|request| { Box::pin(async move { Ok(request.content) }) @@ -2057,6 +2080,32 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { .expect_err("invalid LLM next request should fail"); assert_eq!(invalid_llm_json.code(), tonic::Code::InvalidArgument); + let llm_continuation = state + .insert_continuation(Continuation::llm(Arc::new(|_request| { + Box::pin(async move { + panic!("worker LLM next panic"); + }) + }))) + .expect("panicking LLM continuation should insert"); + let result = service + .llm_next(Request::new(LlmNextRequest { + activation_id: ACTIVATION_ID.into(), + auth_token: AUTH_TOKEN.into(), + continuation_id: llm_continuation, + request: Some( + json_envelope(LLM_REQUEST_SCHEMA, &valid_llm_request()) + .expect("llm request envelope"), + ), + })) + .await + .expect("LLM panic should become a structured result") + .into_inner(); + assert!( + result + .error + .is_some_and(|error| error.message.contains("worker LLM next panic")) + ); + let stream_continuation = state .insert_continuation(Continuation::llm_stream(Arc::new(|_request| { Box::pin(async move { @@ -2097,6 +2146,42 @@ async fn host_runtime_service_covers_continuation_errors_and_stream_items() { other => panic!("expected worker stream error, got {other:?}"), } + let stream_continuation = state + .insert_continuation(Continuation::llm_stream(Arc::new(|_request| { + Box::pin(async move { + Ok(LlmJsonStream::new(futures_util::stream::once(async move { + panic!("worker stream next panic"); + #[allow(unreachable_code)] + Ok(json!({})) + }))) + }) + }))) + .expect("panicking stream continuation should insert"); + let stream_response = service + .llm_stream_next(Request::new(LlmStreamNextRequest { + activation_id: ACTIVATION_ID.into(), + auth_token: AUTH_TOKEN.into(), + continuation_id: stream_continuation, + request: Some( + json_envelope(LLM_REQUEST_SCHEMA, &valid_llm_request()) + .expect("llm request envelope"), + ), + })) + .await + .expect("stream next should return a stream before polling"); + let mut stream = stream_response.into_inner(); + let chunk = stream + .next() + .await + .expect("panicking stream should yield one error") + .expect("panic should be translated into a stream item"); + match chunk.item { + Some(StreamItem::Error(error)) => { + assert!(error.message.contains("worker stream next panic")); + } + other => panic!("expected worker panic error, got {other:?}"), + } + let stream_continuation = state .insert_continuation(Continuation::llm_stream(Arc::new(|_request| { Box::pin(async move { Ok(LlmJsonStream::new(tokio_stream::empty())) }) diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index a89d75961..1e59cabc4 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -8,6 +8,7 @@ use super::*; use std::collections::VecDeque; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; use nemo_relay_plugin::{ NemoRelayNativeLlmNextFn, NemoRelayNativeLlmSanitizeRequestContext, @@ -16,9 +17,16 @@ use nemo_relay_plugin::{ }; use serde_json::json; +use crate::api::optimization::{ + LlmOptimizationRecorder, current_llm_optimization_recorder, scope_llm_optimization_recorder, +}; +use crate::api::runtime::scope_stack::active_event_uuid; +use crate::api::runtime::subscriber_dispatcher::{ + capture_nested_publication_buffer, with_task_publication_context, +}; use crate::api::runtime::{ BuiltinLlmCodec, LlmSanitizeRequestContext, LlmSanitizeResponseContext, NemoRelayContextState, - global_context, + TASK_SCOPE_STACK, global_context, with_active_event_uuid, }; use crate::codec::openai_chat::OpenAIChatCodec; use crate::codec::response::AnnotatedLlmResponse; @@ -81,6 +89,31 @@ unsafe extern "C" fn accept_native_stream_item( true } +#[derive(Default)] +struct NativeStreamCallbackState { + error: Mutex>, + done: AtomicBool, + notified: tokio::sync::Notify, +} + +unsafe extern "C" fn record_native_stream_result( + user_data: *mut c_void, + _chunk_json: *const NemoRelayNativeString, + error: *const NemoRelayNativeString, + done: bool, +) -> bool { + let state = unsafe { &*(user_data as *const NativeStreamCallbackState) }; + if !error.is_null() { + *state + .error + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = read_native_string(error).ok(); + } + state.done.store(done, Ordering::Release); + state.notified.notify_one(); + true +} + unsafe extern "C" fn stop_after_first_native_stream_item( user_data: *mut c_void, _chunk_json: *const NemoRelayNativeString, @@ -307,12 +340,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { ]; for (inner, invocation, expected) in cases { - let next = Arc::new(NativeAsyncNext { - inner, - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + let next = Arc::new(NativeAsyncNext::new(inner, runtime.handle().clone(), None)); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, receiver) = tokio::sync::oneshot::channel(); let completion = Arc::new(NativeAsyncCompletion { @@ -320,6 +348,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + before_settlement_lock: None, _callback_user_data: None, }); let completion_ref = @@ -337,8 +366,8 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { } } - let next = Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(Arc::new(|_request| { Box::pin(async { Ok(LlmJsonStream::new(tokio_stream::iter(vec![ Ok(json!({"chunk": 1})), @@ -346,10 +375,9 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { ]))) }) })), - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + runtime.handle().clone(), + None, + )); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, _receiver) = tokio::sync::oneshot::channel(); let completion = Arc::new(NativeAsyncCompletion { @@ -357,6 +385,7 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + before_settlement_lock: None, _callback_user_data: None, }); let completion_ref = @@ -381,6 +410,398 @@ fn native_async_next_abi_runs_tool_llm_and_stream_continuations() { } } +fn native_continuation_context_observation( + expected_stack: &ScopeStackHandle, + expected_event_uuid: uuid::Uuid, +) -> Json { + json!({ + "scope_stack": Arc::ptr_eq(¤t_scope_stack(), expected_stack), + "active_event_uuid": active_event_uuid() == Some(expected_event_uuid), + "publication_context": crate::api::runtime::subscriber_dispatcher::publication_context::() + .is_some_and(|context| context.as_str() == "native-continuation"), + "publication_buffer": capture_nested_publication_buffer().is_some(), + "optimization_recorder": current_llm_optimization_recorder().is_some(), + }) +} + +#[test] +fn native_async_next_preserves_runtime_context_for_unary_and_stream_continuations() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let expected_stack = create_scope_stack(); + let expected_event_uuid = uuid::Uuid::now_v7(); + let expected = json!({ + "scope_stack": true, + "active_event_uuid": true, + "publication_context": true, + "publication_buffer": true, + "optimization_recorder": true, + }); + + runtime.block_on(TASK_SCOPE_STACK.scope( + expected_stack.clone(), + with_task_publication_context( + Some(Arc::new(String::from("native-continuation"))), + scope_llm_optimization_recorder( + LlmOptimizationRecorder::default(), + with_active_event_uuid( + expected_event_uuid, + crate::api::runtime::subscriber_dispatcher::with_async_publication_context( + crate::api::runtime::subscriber_dispatcher::register_async_publication(), + async { + let unary_stack = expected_stack.clone(); + let unary = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::Tool(Arc::new(move |_value| { + let unary_stack = unary_stack.clone(); + Box::pin(async move { + Ok(native_continuation_context_observation( + &unary_stack, + expected_event_uuid, + )) + }) + })), + runtime.handle().clone(), + None, + )); + let unary_ref = Arc::into_raw(unary) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + next_abort: Mutex::new(None), + before_settlement_lock: None, + _callback_user_data: None, + }); + let completion_ref = Arc::into_raw(Arc::clone(&completion)) + as *const NemoRelayNativeAsyncCompletion; + let invocation = native_string_from_json(&Json::Null).unwrap(); + assert_eq!( + unsafe { + native_async_next_invoke(unary_ref, invocation, completion_ref) + }, + NemoRelayStatus::Ok + ); + assert_eq!( + receiver.await.unwrap().unwrap(), + json!({"result": expected.clone(), "pending_marks": []}) + ); + unsafe { + native_string_free(invocation); + native_async_next_release(unary_ref); + native_async_completion_release(completion_ref); + } + + let stream_stack = expected_stack.clone(); + let (observed_tx, observed_rx) = tokio::sync::oneshot::channel(); + let observed_tx = Arc::new(Mutex::new(Some(observed_tx))); + let stream_next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(Arc::new(move |_request| { + let stream_stack = stream_stack.clone(); + let observed_tx = observed_tx.clone(); + Box::pin(async move { + if let Some(sender) = observed_tx + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + let _ = sender.send( + native_continuation_context_observation( + &stream_stack, + expected_event_uuid, + ), + ); + } + Ok(LlmJsonStream::new(tokio_stream::empty())) + }) + })), + runtime.handle().clone(), + None, + )); + let stream_next_ref = + Arc::into_raw(stream_next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::mpsc::channel(1); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) + as *const NemoRelayNativeAsyncStream; + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: Json::Null, + }) + .unwrap(), + ) + .unwrap(); + assert_eq!( + unsafe { + native_async_next_invoke_stream( + stream_next_ref, + invocation, + stream_ref, + accept_native_stream_item, + ptr::null_mut(), + ) + }, + NemoRelayStatus::Ok + ); + assert_eq!(observed_rx.await.unwrap(), expected); + drop(NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }); + unsafe { + native_string_free(invocation); + native_async_next_release(stream_next_ref); + native_async_stream_release(stream_ref); + } + }, + ), + ), + ), + ), + )); +} + +#[test] +fn native_legacy_next_preserves_runtime_context_for_unary_and_stream_continuations() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let expected_stack = create_scope_stack(); + let expected_event_uuid = uuid::Uuid::now_v7(); + let expected = json!({ + "scope_stack": true, + "active_event_uuid": true, + "publication_context": true, + "publication_buffer": true, + "optimization_recorder": true, + }); + + runtime.block_on(TASK_SCOPE_STACK.scope( + expected_stack.clone(), + with_task_publication_context( + Some(Arc::new(String::from("native-continuation"))), + scope_llm_optimization_recorder( + LlmOptimizationRecorder::default(), + with_active_event_uuid( + expected_event_uuid, + crate::api::runtime::subscriber_dispatcher::with_async_publication_context( + crate::api::runtime::subscriber_dispatcher::register_async_publication(), + async { + let unary_stack = expected_stack.clone(); + let unary_next: ToolExecutionNextFn = Arc::new(move |_value| { + let unary_stack = unary_stack.clone(); + Box::pin(async move { + Ok(native_continuation_context_observation( + &unary_stack, + expected_event_uuid, + )) + }) + }); + let invocation = native_string_from_json(&Json::Null).unwrap(); + let mut output = ptr::null_mut(); + assert_eq!( + unsafe { + native_tool_next( + invocation, + (&unary_next as *const ToolExecutionNextFn) + .cast_mut() + .cast(), + &mut output, + ) + }, + NemoRelayStatus::Ok + ); + let observed: Json = + serde_json::from_str(&read_native_string(output).unwrap()).unwrap(); + assert_eq!(observed, expected); + unsafe { + native_string_free(invocation); + native_string_free(output); + } + + let stream_stack = expected_stack.clone(); + let stream_next: LlmStreamExecutionNextFn = Arc::new(move |_request| { + let stream_stack = stream_stack.clone(); + Box::pin(async move { + Ok(LlmJsonStream::new(futures_util::stream::once(async move { + Ok(native_continuation_context_observation( + &stream_stack, + expected_event_uuid, + )) + }))) + }) + }); + let request = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: Json::Null, + }) + .unwrap(), + ) + .unwrap(); + let mut native_stream = NemoRelayNativeLlmStreamV1::default(); + assert_eq!( + unsafe { + native_llm_stream_next( + request, + (&stream_next as *const LlmStreamExecutionNextFn) + .cast_mut() + .cast(), + &mut native_stream, + ) + }, + NemoRelayStatus::Ok + ); + unsafe { native_string_free(request) }; + + let mut output = ptr::null_mut(); + assert_eq!( + unsafe { + native_stream.next.unwrap()( + native_stream.user_data, + &mut output, + ) + }, + NemoRelayStatus::Ok + ); + let observed: Json = + serde_json::from_str(&read_native_string(output).unwrap()).unwrap(); + assert_eq!(observed, expected); + unsafe { native_string_free(output) }; + drop_native_stream(native_stream); + }, + ), + ), + ), + ), + )); +} + +#[test] +fn native_async_next_panics_settle_unary_and_stream_errors() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::Tool(Arc::new(|_value| { + Box::pin(async move { + panic!("native unary next panic"); + }) + })), + runtime.handle().clone(), + None, + )); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + next_abort: Mutex::new(None), + before_settlement_lock: None, + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let invocation = native_string_from_json(&Json::Null).unwrap(); + assert_eq!( + unsafe { native_async_next_invoke(next_ref, invocation, completion_ref) }, + NemoRelayStatus::Ok + ); + let error = runtime + .block_on(async { tokio::time::timeout(Duration::from_secs(1), receiver).await }) + .expect("panicking unary next should settle") + .unwrap() + .unwrap_err(); + assert!(error.to_string().contains("native unary next panic")); + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_completion_release(completion_ref); + } + + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + Box::pin(async move { + Ok(LlmJsonStream::new(futures_util::stream::once(async move { + panic!("native stream next panic"); + #[allow(unreachable_code)] + Ok(Json::Null) + }))) + }) + })), + runtime.handle().clone(), + None, + )); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, _receiver) = tokio::sync::mpsc::channel(1); + let output_stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, + _callback_user_data: None, + }); + let output_stream_ref = + Arc::into_raw(Arc::clone(&output_stream)) as *const NemoRelayNativeAsyncStream; + let callback_state = Arc::new(NativeStreamCallbackState::default()); + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: Json::Null, + }) + .unwrap(), + ) + .unwrap(); + assert_eq!( + unsafe { + native_async_next_invoke_stream( + next_ref, + invocation, + output_stream_ref, + record_native_stream_result, + Arc::as_ptr(&callback_state).cast_mut().cast(), + ) + }, + NemoRelayStatus::Ok + ); + runtime + .block_on(async { + tokio::time::timeout(Duration::from_secs(1), callback_state.notified.notified()).await + }) + .expect("panicking stream next should report an error"); + assert!(!callback_state.done.load(Ordering::Acquire)); + assert!( + callback_state + .error + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .as_deref() + .is_some_and(|error| error.contains("native stream next panic")) + ); + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + native_async_stream_release(output_stream_ref); + } +} + #[test] fn native_async_next_is_permanently_one_shot() { let runtime = tokio::runtime::Builder::new_current_thread() @@ -388,8 +809,8 @@ fn native_async_next_is_permanently_one_shot() { .build() .unwrap(); let calls = Arc::new(AtomicUsize::new(0)); - let next = Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::Tool({ + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::Tool({ let calls = Arc::clone(&calls); Arc::new(move |value| { let calls = Arc::clone(&calls); @@ -399,10 +820,9 @@ fn native_async_next_is_permanently_one_shot() { }) }) }), - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + runtime.handle().clone(), + None, + )); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, receiver) = tokio::sync::oneshot::channel(); let completion = Arc::new(NativeAsyncCompletion { @@ -410,6 +830,7 @@ fn native_async_next_is_permanently_one_shot() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + before_settlement_lock: None, _callback_user_data: None, }); let completion_ref = @@ -435,20 +856,117 @@ fn native_async_next_is_permanently_one_shot() { } } +#[test] +fn cancelled_native_async_next_does_not_start_unary_or_stream_continuations() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let unary_started = Arc::new(AtomicBool::new(false)); + let unary = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::Tool({ + let unary_started = unary_started.clone(); + Arc::new(move |value| { + unary_started.store(true, Ordering::SeqCst); + Box::pin(async move { Ok(value) }) + }) + }), + runtime.handle().clone(), + None, + )); + let unary_ref = Arc::into_raw(unary) as *const NemoRelayNativeAsyncNext; + let (sender, _receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(true), + next_invoked: AtomicBool::new(false), + next_abort: Mutex::new(None), + before_settlement_lock: None, + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let unary_invocation = native_string_from_json(&Json::Null).unwrap(); + assert_eq!( + unsafe { native_async_next_invoke(unary_ref, unary_invocation, completion_ref) }, + NemoRelayStatus::InvalidArg + ); + + let stream_started = Arc::new(AtomicBool::new(false)); + let stream_next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream({ + let stream_started = stream_started.clone(); + Arc::new(move |_request| { + stream_started.store(true, Ordering::SeqCst); + Box::pin(async { Ok(LlmJsonStream::new(tokio_stream::empty())) }) + }) + }), + runtime.handle().clone(), + None, + )); + let stream_next_ref = Arc::into_raw(stream_next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::mpsc::channel(1); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(true), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; + let stream_invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: Json::Null, + }) + .unwrap(), + ) + .unwrap(); + assert_eq!( + unsafe { + native_async_next_invoke_stream( + stream_next_ref, + stream_invocation, + stream_ref, + accept_native_stream_item, + ptr::null_mut(), + ) + }, + NemoRelayStatus::InvalidArg + ); + runtime.block_on(tokio::task::yield_now()); + assert!(!unary_started.load(Ordering::SeqCst)); + assert!(!stream_started.load(Ordering::SeqCst)); + + drop(NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }); + unsafe { + native_string_free(unary_invocation); + native_string_free(stream_invocation); + native_async_next_release(unary_ref); + native_async_next_release(stream_next_ref); + native_async_completion_release(completion_ref); + native_async_stream_release(stream_ref); + } +} + #[test] fn malformed_llm_next_does_not_consume_the_completion() { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); - let next = Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::Llm(Arc::new(|request| { + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::Llm(Arc::new(|request| { Box::pin(async move { Ok(request.content) }) })), - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + runtime.handle().clone(), + None, + )); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, _receiver) = tokio::sync::oneshot::channel(); let completion = Arc::new(NativeAsyncCompletion { @@ -456,6 +974,7 @@ fn malformed_llm_next_does_not_consume_the_completion() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + before_settlement_lock: None, _callback_user_data: None, }); let completion_ref = @@ -488,14 +1007,13 @@ fn native_async_stream_next_is_one_shot() { .enable_all() .build() .unwrap(); - let next = Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(Arc::new(|_request| { Box::pin(async { Ok(LlmJsonStream::new(tokio_stream::empty())) }) })), - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + runtime.handle().clone(), + None, + )); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, receiver) = tokio::sync::mpsc::channel(1); let stream = Arc::new(NativeAsyncStream { @@ -503,6 +1021,8 @@ fn native_async_stream_next_is_one_shot() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, _callback_user_data: None, }); let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; @@ -559,8 +1079,8 @@ fn native_async_stream_next_stops_callbacks_after_false() { .enable_all() .build() .unwrap(); - let next = Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::LlmStream(Arc::new(|_request| { + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(Arc::new(|_request| { Box::pin(async { Ok(LlmJsonStream::new(tokio_stream::iter(vec![ Ok(json!({"chunk": 1})), @@ -568,10 +1088,9 @@ fn native_async_stream_next_stops_callbacks_after_false() { ]))) }) })), - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + runtime.handle().clone(), + None, + )); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, receiver) = tokio::sync::mpsc::channel(1); let stream = Arc::new(NativeAsyncStream { @@ -579,6 +1098,8 @@ fn native_async_stream_next_stops_callbacks_after_false() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, _callback_user_data: None, }); let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; @@ -630,8 +1151,8 @@ fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { .unwrap(); let (started_tx, started_rx) = tokio::sync::oneshot::channel(); let started_tx = Arc::new(Mutex::new(Some(started_tx))); - let next = Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::LlmStream(Arc::new(move |_request| { + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(Arc::new(move |_request| { let started_tx = Arc::clone(&started_tx); Box::pin(async move { if let Some(started_tx) = started_tx.lock().unwrap().take() { @@ -640,10 +1161,9 @@ fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { std::future::pending().await }) })), - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + runtime.handle().clone(), + None, + )); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, receiver) = tokio::sync::mpsc::channel(1); let stream = Arc::new(NativeAsyncStream { @@ -651,6 +1171,8 @@ fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, _callback_user_data: None, }); let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; @@ -703,6 +1225,7 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + before_settlement_lock: None, _callback_user_data: None, }); let completion_ref = @@ -737,6 +1260,7 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen cancelled: AtomicBool::new(true), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + before_settlement_lock: None, _callback_user_data: None, }); let completion_ref = @@ -750,6 +1274,112 @@ fn native_async_completion_abi_rejects_invalid_duplicate_and_cancelled_settlemen unsafe { native_async_completion_release(completion_ref) }; } +#[test] +fn completed_native_async_wait_is_not_marked_cancelled() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (sender, receiver) = tokio::sync::oneshot::channel(); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + next_abort: Mutex::new(None), + before_settlement_lock: None, + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let value = native_string(r#"{"ok":true}"#); + let mut wait = NativeAsyncWait { + completion: Arc::clone(&completion), + receiver, + completed: false, + }; + + assert_eq!( + unsafe { native_async_completion_resolve_json(completion_ref, value) }, + NemoRelayStatus::Ok + ); + assert_eq!( + runtime.block_on(wait.receive()).unwrap(), + json!({"ok": true}) + ); + drop(wait); + assert!(!completion.cancelled.load(Ordering::Acquire)); + assert!(!unsafe { native_async_completion_is_cancelled(completion_ref) }); + + unsafe { + native_string_free(value); + native_async_completion_release(completion_ref); + } +} + +#[test] +fn native_async_completion_cancellation_wins_resolve_and_reject_settlement_races() { + type SettleFn = unsafe extern "C" fn( + *const NemoRelayNativeAsyncCompletion, + *const NemoRelayNativeString, + ) -> NemoRelayStatus; + + for (settle, argument) in [ + ( + native_async_completion_resolve_json as SettleFn, + native_string(r#"{"ok":true}"#), + ), + ( + native_async_completion_reject as SettleFn, + native_string("cancelled"), + ), + ] { + let (sender, _receiver) = tokio::sync::oneshot::channel(); + let settlement_checkpoint = Arc::new(std::sync::Barrier::new(2)); + let completion = Arc::new(NativeAsyncCompletion { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + next_abort: Mutex::new(None), + before_settlement_lock: Some(Arc::clone(&settlement_checkpoint)), + _callback_user_data: None, + }); + let completion_ref = + Arc::into_raw(Arc::clone(&completion)) as *const NemoRelayNativeAsyncCompletion; + let cancellation_guard = completion + .next_abort + .lock() + .unwrap_or_else(|error| error.into_inner()); + let completion_address = completion_ref as usize; + let argument_address = argument as usize; + let settlement = std::thread::spawn(move || unsafe { + settle( + completion_address as *const NemoRelayNativeAsyncCompletion, + argument_address as *const NemoRelayNativeString, + ) + }); + + // The settlement thread has parsed its argument and reached the exact + // boundary before acquiring next_abort. The held guard now forces it + // to observe cancellation after the lock is released. + settlement_checkpoint.wait(); + completion.cancelled.store(true, Ordering::Release); + drop(cancellation_guard); + + assert_eq!(settlement.join().unwrap(), NemoRelayStatus::InvalidArg); + assert!( + completion + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_some() + ); + unsafe { + native_string_free(argument); + native_async_completion_release(completion_ref); + } + } +} + #[test] fn cancelling_completion_aborts_pending_native_next() { struct DropProbe(Arc); @@ -766,8 +1396,8 @@ fn cancelling_completion_aborts_pending_native_next() { .unwrap(); let started = Arc::new(AtomicBool::new(false)); let dropped = Arc::new(AtomicBool::new(false)); - let next = Arc::new(NativeAsyncNext { - inner: NativeAsyncNextInner::Llm({ + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::Llm({ let started = Arc::clone(&started); let dropped = Arc::clone(&dropped); Arc::new(move |_request| { @@ -781,10 +1411,9 @@ fn cancelling_completion_aborts_pending_native_next() { }) }) }), - runtime: runtime.handle().clone(), - scope_stack: current_scope_stack(), - _callback_user_data: None, - }); + runtime.handle().clone(), + None, + )); let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; let (sender, receiver) = tokio::sync::oneshot::channel(); let completion = Arc::new(NativeAsyncCompletion { @@ -792,6 +1421,7 @@ fn cancelling_completion_aborts_pending_native_next() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), next_abort: Mutex::new(None), + before_settlement_lock: None, _callback_user_data: None, }); let completion_ref = @@ -814,6 +1444,7 @@ fn cancelling_completion_aborts_pending_native_next() { drop(NativeAsyncWait { completion: Arc::clone(&completion), receiver, + completed: false, }); runtime.block_on(tokio::task::yield_now()); assert!(completion.cancelled.load(Ordering::SeqCst)); @@ -833,6 +1464,83 @@ fn cancelling_completion_aborts_pending_native_next() { } } +#[test] +fn native_async_stream_settlement_cannot_succeed_after_cancellation() { + #[derive(Clone, Copy)] + enum Settlement { + Push(usize), + Finish, + Reject(usize), + } + + for settlement in [ + Settlement::Push(native_string(r#"{"chunk":1}"#) as usize), + Settlement::Finish, + Settlement::Reject(native_string("cancelled") as usize), + ] { + let (sender, receiver) = tokio::sync::mpsc::channel(1); + let settlement_checkpoint = Arc::new(std::sync::Barrier::new(2)); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: Some(Arc::clone(&settlement_checkpoint)), + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; + let cancellation_guard = stream + .settlement + .lock() + .unwrap_or_else(|error| error.into_inner()); + let stream_address = stream_ref as usize; + let operation = std::thread::spawn(move || unsafe { + match settlement { + Settlement::Push(chunk) => native_async_stream_push_json( + stream_address as *const NemoRelayNativeAsyncStream, + chunk as *const NemoRelayNativeString, + ), + Settlement::Finish => { + native_async_stream_finish(stream_address as *const NemoRelayNativeAsyncStream) + } + Settlement::Reject(message) => native_async_stream_reject( + stream_address as *const NemoRelayNativeAsyncStream, + message as *const NemoRelayNativeString, + ), + } + }); + + settlement_checkpoint.wait(); + stream.cancelled.store(true, Ordering::Release); + drop(cancellation_guard); + + assert_eq!(operation.join().unwrap(), NemoRelayStatus::InvalidArg); + assert!( + stream + .sender + .lock() + .unwrap_or_else(|error| error.into_inner()) + .is_some() + ); + + let argument = match settlement { + Settlement::Push(argument) | Settlement::Reject(argument) => Some(argument), + Settlement::Finish => None, + }; + drop(NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }); + unsafe { + if let Some(argument) = argument { + native_string_free(argument as *mut NemoRelayNativeString); + } + native_async_stream_release(stream_ref); + } + } +} + #[test] fn native_async_stream_push_is_bounded_retryable_and_incremental() { let runtime = tokio::runtime::Builder::new_current_thread() @@ -845,6 +1553,8 @@ fn native_async_stream_push_is_bounded_retryable_and_incremental() { cancelled: AtomicBool::new(false), next_invoked: AtomicBool::new(false), downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, _callback_user_data: None, }); let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; diff --git a/crates/core/tests/unit/observability/mod_tests.rs b/crates/core/tests/unit/observability/mod_tests.rs new file mode 100644 index 000000000..0982668cf --- /dev/null +++ b/crates/core/tests/unit/observability/mod_tests.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::{relay_span_id, relay_trace_id}; +use uuid::Uuid; + +#[test] +fn relay_id_conversions_preserve_zero_bytes() { + let uuid = Uuid::nil(); + + assert_eq!(relay_trace_id(uuid).to_bytes(), [0; 16]); + assert_eq!(relay_span_id(uuid).to_bytes(), [0; 8]); +} diff --git a/crates/ffi/nemo_relay.h b/crates/ffi/nemo_relay.h index accaa5e9e..6517cfc2e 100644 --- a/crates/ffi/nemo_relay.h +++ b/crates/ffi/nemo_relay.h @@ -451,6 +451,9 @@ typedef char *(*NemoRelayToolExecCb)(void *user_data, const char *args_json); * This helper applies only the request-intercept middleware and does not emit * lifecycle events or execute the tool callback. * + * This legacy helper blocks its caller. If called from a Tokio runtime, + * middleware must not depend on work driven exclusively by that caller thread. + * * # Parameters * - `name`: Tool name (null-terminated C string). * - `args_json`: Tool arguments as a JSON C string. @@ -471,6 +474,9 @@ NemoRelayStatus nemo_relay_tool_request_intercepts(const char *name, /** * Run the registered tool conditional execution guardrail chain. * + * This legacy helper blocks its caller. If called from a Tokio runtime, + * middleware must not depend on work driven exclusively by that caller thread. + * * Returns `NemoRelayStatus::Ok` if all guardrails pass, or * `NemoRelayStatus::GuardrailRejected` if blocked. * @@ -493,6 +499,9 @@ NemoRelayStatus nemo_relay_tool_conditional_execution(const char *name, const ch * This helper applies only the request-intercept middleware and does not emit * lifecycle events or execute the provider callback. * + * This legacy helper blocks its caller. If called from a Tokio runtime, + * middleware must not depend on work driven exclusively by that caller thread. + * * # Parameters * - `name`: Optional provider name as a null-terminated C string. Pass null to * use an empty logical name. @@ -565,6 +574,9 @@ NemoRelayStatus nemo_relay_llm_request_intercept_outcome_json_new_v2(const struc /** * Run the registered LLM conditional execution guardrail chain. * + * This legacy helper blocks its caller. If called from a Tokio runtime, + * middleware must not depend on work driven exclusively by that caller thread. + * * Returns `NemoRelayStatus::Ok` if all guardrails pass, or * `NemoRelayStatus::GuardrailRejected` if blocked. * diff --git a/crates/ffi/src/api/mod.rs b/crates/ffi/src/api/mod.rs index 1fc8977fd..cd08d8d03 100644 --- a/crates/ffi/src/api/mod.rs +++ b/crates/ffi/src/api/mod.rs @@ -48,14 +48,14 @@ use nemo_relay::api::registry as core_registry_api; use nemo_relay::api::runtime::{LlmExecutionNextFn, LlmStreamExecutionNextFn, ToolExecutionNextFn}; use nemo_relay::api::runtime::{ TASK_SCOPE_STACK, capture_thread_scope_stack, create_scope_stack, current_scope_stack, - restore_thread_scope_stack, scope_stack_active, set_thread_scope_stack, + restore_thread_scope_stack, scope_stack_active, set_thread_scope_stack, with_scope_stack, }; use nemo_relay::api::scope as core_scope_api; use nemo_relay::api::scope::ScopeAttributes; use nemo_relay::api::subscriber as core_subscriber_api; use nemo_relay::api::tool as core_tool_api; use nemo_relay::api::tool::ToolAttributes; -use nemo_relay::error::Result as FlowResult; +use nemo_relay::error::{FlowError, Result as FlowResult}; use nemo_relay::plugin::dynamic::{DynamicPluginActivationSpec, PluginHostActivation}; use nemo_relay::plugin::{ ConfigDiagnostic, DiagnosticLevel, Plugin, PluginConfig, PluginError, @@ -99,13 +99,30 @@ fn tokio_runtime() -> &'static Runtime { }) } -fn block_on_sync_ffi(future: impl Future>) -> FlowResult { - // Embedded hosts must not call synchronous middleware helpers from a Tokio - // runtime thread. Use the completion-based async registration API there. +fn block_on_sync_ffi(future: F) -> FlowResult +where + T: Send, + F: Future> + Send, +{ + // These legacy helpers remain synchronous for source compatibility. When + // called from Tokio, the caller thread waits while Relay polls the chain on + // its runtime. Middleware must not depend on work driven exclusively by + // that blocked caller thread. if tokio::runtime::Handle::try_current().is_ok() { - return Err(nemo_relay::error::FlowError::Internal( - "synchronous FFI middleware helpers cannot run on a Tokio runtime thread; use the completion-based async registration API".into(), - )); + let effective_scope_stack = current_scope_stack(); + return std::thread::scope(|scope| { + scope + .spawn(move || { + with_scope_stack(effective_scope_stack, || tokio_runtime().block_on(future)) + }) + .join() + }) + .map_err(|_| { + FlowError::Internal( + "synchronous FFI middleware helper thread panicked while awaiting middleware" + .into(), + ) + })?; } tokio_runtime().block_on(future) } @@ -119,6 +136,9 @@ fn block_on_sync_ffi(future: impl Future>) -> FlowResu /// This helper applies only the request-intercept middleware and does not emit /// lifecycle events or execute the tool callback. /// +/// This legacy helper blocks its caller. If called from a Tokio runtime, +/// middleware must not depend on work driven exclusively by that caller thread. +/// /// # Parameters /// - `name`: Tool name (null-terminated C string). /// - `args_json`: Tool arguments as a JSON C string. @@ -163,6 +183,9 @@ pub unsafe extern "C" fn nemo_relay_tool_request_intercepts( /// Run the registered tool conditional execution guardrail chain. /// +/// This legacy helper blocks its caller. If called from a Tokio runtime, +/// middleware must not depend on work driven exclusively by that caller thread. +/// /// Returns `NemoRelayStatus::Ok` if all guardrails pass, or /// `NemoRelayStatus::GuardrailRejected` if blocked. /// @@ -201,6 +224,9 @@ pub unsafe extern "C" fn nemo_relay_tool_conditional_execution( /// This helper applies only the request-intercept middleware and does not emit /// lifecycle events or execute the provider callback. /// +/// This legacy helper blocks its caller. If called from a Tokio runtime, +/// middleware must not depend on work driven exclusively by that caller thread. +/// /// # Parameters /// - `name`: Optional provider name as a null-terminated C string. Pass null to /// use an empty logical name. @@ -378,6 +404,9 @@ unsafe fn parse_optional_intercept_json( /// Run the registered LLM conditional execution guardrail chain. /// +/// This legacy helper blocks its caller. If called from a Tokio runtime, +/// middleware must not depend on work driven exclusively by that caller thread. +/// /// Returns `NemoRelayStatus::Ok` if all guardrails pass, or /// `NemoRelayStatus::GuardrailRejected` if blocked. /// diff --git a/crates/ffi/tests/unit/api/core_tests.rs b/crates/ffi/tests/unit/api/core_tests.rs index 6b1320e5f..ad1a769a3 100644 --- a/crates/ffi/tests/unit/api/core_tests.rs +++ b/crates/ffi/tests/unit/api/core_tests.rs @@ -911,6 +911,56 @@ fn test_ffi_tool_lifecycle_execute_and_helpers() { } } +#[test] +fn synchronous_ffi_middleware_helper_works_inside_tokio_with_scope_local_visibility() { + let _lock = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + reset_globals(); + + unsafe { + let mut scope = ptr::null_mut(); + assert_eq!(api::nemo_relay_get_handle(&mut scope), NemoRelayStatus::Ok); + let scope_uuid = cstring( + &take_string(nemo_relay_scope_handle_uuid(scope)) + .expect("current scope should have a UUID"), + ); + let intercept_name = cstring(&unique_name("ffi_tokio_scope_intercept")); + assert_eq!( + nemo_relay_scope_register_tool_request_intercept( + scope_uuid.as_ptr(), + intercept_name.as_ptr(), + 1, + false, + tool_request_cb, + ptr::null_mut(), + None, + ), + NemoRelayStatus::Ok + ); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let tool_name = cstring("ffi_tokio_tool"); + let args = cstring(r#"{"value":1}"#); + let mut output = ptr::null_mut(); + let status = runtime.block_on(async { + nemo_relay_tool_request_intercepts(tool_name.as_ptr(), args.as_ptr(), &mut output) + }); + assert_eq!(status, NemoRelayStatus::Ok); + assert_eq!(returned_json(output)["intercepted"], json!(true)); + + assert_eq!( + nemo_relay_scope_deregister_tool_request_intercept( + scope_uuid.as_ptr(), + intercept_name.as_ptr(), + ), + NemoRelayStatus::Ok + ); + nemo_relay_scope_handle_free(scope); + } +} + #[test] fn test_ffi_manual_lifecycle_timestamps_accept_unix_micros() { let _lock = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 44cbb97e1..7689c900c 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -1801,21 +1801,40 @@ pub fn test_closed_finalizer_callback( wrapped() } -/// Internal test helper: exercise the PromiseAwareFn closed-call path. +/// Internal test helper: exercise PromiseAwareFn queue and conversion failures. #[napi( js_name = "__testClosedPromiseAwareCall", ts_return_type = "Promise" )] -pub fn test_closed_promise_aware_call(env: Env, func: JsFunction) -> Result { +pub fn test_closed_promise_aware_call( + env: Env, + func: JsFunction, + force_conversion_failure: Option, +) -> Result { let promise_aware = std::sync::Arc::new( crate::promise_call::PromiseAwareFn::new(&env, &func).map_err(|e| { napi::Error::from_reason(format!("failed to create PromiseAwareFn: {e}")) })?, ); - promise_aware.close(); + if !force_conversion_failure.unwrap_or(false) { + promise_aware.close(); + } env.execute_tokio_future( - async move { promise_aware.call(Json::Null).await.map_err(to_napi_err) }, + async move { + if force_conversion_failure.unwrap_or(false) { + promise_aware + .call_with_arg0(Box::new(|_| { + Err(napi::Error::from_reason( + "forced PromiseAwareFn conversion failure", + )) + })) + .await + } else { + promise_aware.call(Json::Null).await + } + .map_err(to_napi_err) + }, |_env, result| Ok(result), ) } @@ -3216,10 +3235,10 @@ pub fn deregister_subscriber(name: String) -> Result { /// Return a Promise that resolves when native subscriber callbacks queued /// before this call finish. /// -/// When called from a queued publication sanitizer callback (including event and manual tool/LLM -/// sanitizers), this Promise resolves without waiting to prevent a cycle with the serial -/// dispatcher. Publication middleware must not move such a re-entrant flush to -/// an unmarked worker thread. +/// Call this function outside subscribers, event sanitizers, conditional +/// guardrails, and request or execution intercepts. A queued tool or LLM +/// observability sanitizer may call it, but the Promise resolves without +/// waiting for its own publication. /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this /// Promise does not block the Node event loop while Promise-returning event sanitizers settle. @@ -3228,7 +3247,7 @@ pub fn deregister_subscriber(name: String) -> Result { /// Callers should handle errors when awaiting it. #[napi(ts_return_type = "Promise")] pub fn flush_subscribers(env: Env) -> Result { - let reentrant = crate::callback_factory::event_sanitizer_callback_active(&env)?; + let reentrant = crate::callback_factory::publication_callback_active(&env)?; env.execute_tokio_future( async move { if reentrant { @@ -4117,23 +4136,23 @@ impl AtofExporter { .map_err(|e| napi::Error::from_reason(e.to_string())) } - /// Outside a native subscriber callback, wait for queued subscriber delivery, then flush the - /// file sink or ask the stream sink to drain for up to its timeout. A re-entrant call does not - /// establish the delivery barrier. A stream timeout is logged and does not by itself return an - /// error. + /// Outside subscriber and middleware callbacks, wait for queued subscriber delivery, then + /// flush the file sink or ask the stream sink to drain for up to its timeout. A stream timeout + /// is logged and does not by itself return an error. #[napi] - pub fn force_flush(&self, env: Env) -> napi::Result<()> { - with_effective_scope_stack(&env, || self.inner.force_flush())? + pub fn force_flush(&self) -> napi::Result<()> { + self.inner + .force_flush() .map_err(|e| napi::Error::from_reason(e.to_string())) } - /// Outside a native subscriber callback, wait for queued subscriber delivery, then flush the - /// file sink or ask the stream sink to drain and close up to its timeout. A re-entrant call - /// does not establish the delivery barrier. A stream timeout is logged and does not by itself - /// return an error. + /// Outside subscriber and middleware callbacks, wait for queued subscriber delivery, then + /// flush the file sink or ask the stream sink to drain and close up to its timeout. A stream + /// timeout is logged and does not by itself return an error. #[napi] - pub fn shutdown(&self, env: Env) -> napi::Result<()> { - with_effective_scope_stack(&env, || self.inner.shutdown())? + pub fn shutdown(&self) -> napi::Result<()> { + self.inner + .shutdown() .map_err(|e| napi::Error::from_reason(e.to_string())) } } @@ -4208,15 +4227,17 @@ impl OpenTelemetrySubscriber { /// Force a flush of finished spans through the exporter. #[napi] - pub fn force_flush(&self, env: Env) -> napi::Result<()> { - with_effective_scope_stack(&env, || self.inner.force_flush())? + pub fn force_flush(&self) -> napi::Result<()> { + self.inner + .force_flush() .map_err(|e| napi::Error::from_reason(e.to_string())) } /// Shut down the underlying tracer provider. #[napi] - pub fn shutdown(&self, env: Env) -> napi::Result<()> { - with_effective_scope_stack(&env, || self.inner.shutdown())? + pub fn shutdown(&self) -> napi::Result<()> { + self.inner + .shutdown() .map_err(|e| napi::Error::from_reason(e.to_string())) } } diff --git a/crates/node/src/callable.rs b/crates/node/src/callable.rs index a9e673a37..731348696 100644 --- a/crates/node/src/callable.rs +++ b/crates/node/src/callable.rs @@ -480,7 +480,8 @@ pub fn wrap_js_llm_request_intercept_promise_fn( /// Wrap a Promise-aware JS event sanitizer. /// /// All lifecycle publication invokes these callbacks from Relay's serial -/// dispatcher. The invocation context marks `flushSubscribers()` as reentrant. +/// dispatcher. The invocation context is also used by queued tool and LLM +/// observability sanitizers so a flush cannot wait on its own publication. pub fn wrap_js_event_sanitize_promise_fn(func: Arc) -> EventSanitizeFn { Arc::new(move |event: Arc, fields: CoreEventSanitizeFields| { let func = func.clone(); diff --git a/crates/node/src/callback_factory.rs b/crates/node/src/callback_factory.rs index b44d76a7d..40503defa 100644 --- a/crates/node/src/callback_factory.rs +++ b/crates/node/src/callback_factory.rs @@ -10,7 +10,7 @@ use nemo_relay::api::runtime::subscriber_dispatcher::PublicationBuffer; use crate::types::ScopeStack; -const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v4"; +const CALLBACK_FACTORIES_PROPERTY: &str = "__nemo_relay_callback_factories_v5"; const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { const { AsyncLocalStorage } = process.getBuiltinModule('node:async_hooks'); @@ -164,7 +164,9 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { try { message = String(error?.message ?? error); } catch {} - reject(message); + if (typeof reject === 'function') { + reject(message); + } return; } callPromise( @@ -181,7 +183,7 @@ const CALLBACK_FACTORIES_SOURCE: &str = r#"(() => { }; }, - eventSanitizerCallbackActive() { + publicationCallbackActive() { return eventSanitizerContext.getStore()?.publicationState.active === true; }, @@ -268,9 +270,9 @@ pub(crate) fn wrap_promise_callback(env: &Env, func: &JsFunction) -> napi::Resul wrap_callback(env, func, "promise") } -pub(crate) fn event_sanitizer_callback_active(env: &Env) -> napi::Result { +pub(crate) fn publication_callback_active(env: &Env) -> napi::Result { let factories = callback_factories(env)?; - let callback: JsFunction = factories.get_named_property("eventSanitizerCallbackActive")?; + let callback: JsFunction = factories.get_named_property("publicationCallbackActive")?; callback .call::(None, &[])? .coerce_to_bool()? diff --git a/crates/node/src/promise_call.rs b/crates/node/src/promise_call.rs index 8ee3ad7f6..14627fe49 100644 --- a/crates/node/src/promise_call.rs +++ b/crates/node/src/promise_call.rs @@ -23,7 +23,9 @@ use serde_json::Value as Json; use nemo_relay::api::runtime::subscriber_dispatcher::{ PublicationBuffer, capture_nested_publication_buffer, with_task_nested_publication_buffer, }; -use nemo_relay::api::runtime::{ScopeStackHandle, TASK_SCOPE_STACK, current_scope_stack}; +use nemo_relay::api::runtime::{ + MiddlewareContinuationContext, ScopeStackHandle, current_scope_stack, +}; use nemo_relay::error::{FlowError, Result as FlowResult}; use crate::callback_factory; @@ -87,6 +89,7 @@ struct CallArgs { /// Scope stack captured when Relay invokes the middleware. scope_stack: Option, publication_buffer: Option, + continuation_context: Option, completion: CallCompletion, } @@ -161,29 +164,27 @@ fn undefined_to_unknown(env: &Env) -> napi::Result { fn build_next_unknown( env: &Env, next: NextFn, - scope_stack: ScopeStackHandle, + continuation_context: MiddlewareContinuationContext, publication_context_id: Option, - publication_buffer: Option, ) -> napi::Result { let next_fn = match next { NextFn::Json(next) => { env.create_function_from_closure("__nemo_relay_next", move |ctx| { let arg = ctx.get::(0).unwrap_or(Json::Null); let next = next.clone(); - let scope_stack = scope_stack.clone(); + let continuation_context = continuation_context.clone(); let publication_context_id = publication_context_id.clone(); - let publication_buffer = publication_buffer.clone(); ctx.env.execute_tokio_future( async move { with_publication_callback_context( publication_context_id, - publication_buffer, + None, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - next(arg) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) + continuation_context + .invoke(move || async move { + next(arg).await.map_err(|error| { + napi::Error::from_reason(error.to_string()) + }) }) .await }, @@ -198,20 +199,19 @@ fn build_next_unknown( env.create_function_from_closure("__nemo_relay_next", move |ctx| { let arg = ctx.get::(0).unwrap_or(Json::Null); let next = next.clone(); - let scope_stack = scope_stack.clone(); + let continuation_context = continuation_context.clone(); let publication_context_id = publication_context_id.clone(); - let publication_buffer = publication_buffer.clone(); ctx.env.execute_tokio_future( async move { with_publication_callback_context( publication_context_id, - publication_buffer, + None, async move { - TASK_SCOPE_STACK - .scope(scope_stack, async move { - next(arg) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) + continuation_context + .invoke(move || async move { + next(arg).await.map_err(|error| { + napi::Error::from_reason(error.to_string()) + }) }) .await }, @@ -277,67 +277,80 @@ impl PromiseAwareFn { fn from_wrapper(env: &Env, wrapper: &JsFunction) -> napi::Result { let mut tsfn = env.create_threadsafe_function(wrapper, 0, |ctx: ThreadSafeCallContext| { - let next = match ctx.value.next { - Some(next) => { - let scope_stack = ctx.value.scope_stack.clone().ok_or_else(|| { - napi::Error::from_reason( - "middleware next callback is missing its captured scope stack", - ) - })?; - build_next_unknown( - &ctx.env, - next, - scope_stack, - ctx.value.publication_context_id.clone(), - ctx.value.publication_buffer.clone(), - )? - } - None => undefined_to_unknown(&ctx.env)?, - }; - let (resolve, reject) = build_completion_unknowns(&ctx.env, ctx.value.completion)?; - let arg0 = match ctx.value.arg0 { - PrimaryArg::Json(value) => json_to_unknown(&ctx.env, value)?, - PrimaryArg::Build(build) => build(&ctx.env)?, - }; - - let spread = unsafe { - JsUnknown::from_raw_unchecked( - ctx.env.raw(), - ctx.env.get_boolean(ctx.value.spread)?.raw(), - ) - }; - let publication = unsafe { - JsUnknown::from_raw_unchecked( - ctx.env.raw(), - ctx.env.get_boolean(ctx.value.publication)?.raw(), - ) - }; - let publication_context_id = match ctx.value.publication_context_id { - Some(context_id) => json_to_unknown(&ctx.env, Json::String(context_id))?, - None => undefined_to_unknown(&ctx.env)?, - }; - let scope_stack = match ctx.value.scope_stack { - Some(scope_stack) => { - let scope_stack = ScopeStack { - inner: scope_stack, - publication_buffer: ctx.value.publication_buffer, + let completion = ctx.value.completion.clone(); + let result = (|| { + let next = match ctx.value.next { + Some(next) => { + let continuation_context = ctx + .value + .continuation_context + .clone() + .ok_or_else(|| { + napi::Error::from_reason( + "middleware next callback is missing its captured Relay context", + ) + })?; + build_next_unknown( + &ctx.env, + next, + continuation_context, + ctx.value.publication_context_id.clone(), + )? } - .into_instance(ctx.env)?; - unsafe { JsUnknown::from_raw_unchecked(ctx.env.raw(), scope_stack.raw()) } - } - None => undefined_to_unknown(&ctx.env)?, - }; - let args = vec![ - arg0, - spread, - next, - resolve, - reject, - publication, - publication_context_id, - scope_stack, - ]; - Ok(args) + None => undefined_to_unknown(&ctx.env)?, + }; + let arg0 = match ctx.value.arg0 { + PrimaryArg::Json(value) => json_to_unknown(&ctx.env, value)?, + PrimaryArg::Build(build) => build(&ctx.env)?, + }; + let spread = unsafe { + JsUnknown::from_raw_unchecked( + ctx.env.raw(), + ctx.env.get_boolean(ctx.value.spread)?.raw(), + ) + }; + let publication = unsafe { + JsUnknown::from_raw_unchecked( + ctx.env.raw(), + ctx.env.get_boolean(ctx.value.publication)?.raw(), + ) + }; + let publication_context_id = match ctx.value.publication_context_id { + Some(context_id) => json_to_unknown(&ctx.env, Json::String(context_id))?, + None => undefined_to_unknown(&ctx.env)?, + }; + let scope_stack = match ctx.value.scope_stack { + Some(scope_stack) => { + let scope_stack = ScopeStack { + inner: scope_stack, + publication_buffer: ctx.value.publication_buffer, + } + .into_instance(ctx.env)?; + unsafe { + JsUnknown::from_raw_unchecked(ctx.env.raw(), scope_stack.raw()) + } + } + None => undefined_to_unknown(&ctx.env)?, + }; + let (resolve, reject) = + build_completion_unknowns(&ctx.env, ctx.value.completion)?; + Ok(vec![ + arg0, + spread, + next, + resolve, + reject, + publication, + publication_context_id, + scope_stack, + ]) + })(); + if let Err(error) = &result { + completion.send(Err(FlowError::Internal(format!( + "failed to build JavaScript middleware callback arguments: {error}" + )))); + } + result })?; // The callback should not keep the Node event loop alive on its own. @@ -444,6 +457,9 @@ impl PromiseAwareFn { next: Option, ) -> FlowResult { let (sender, receiver) = tokio::sync::oneshot::channel(); + let continuation_context = next + .as_ref() + .map(|_| MiddlewareContinuationContext::capture()); let tsfn = self .tsfn .lock() @@ -458,10 +474,12 @@ impl PromiseAwareFn { next, publication: mode.publication, publication_context_id: publication_callback_context_id(), - // Scope identity applies to every middleware callback. The - // publication bit controls only re-entrant flush behavior. + // Scope identity applies to every middleware callback. + // Publication context also lets queued tool/LLM observability + // sanitizers avoid waiting on their own publication. scope_stack: Some(current_scope_stack()), publication_buffer: capture_nested_publication_buffer(), + continuation_context, completion: CallCompletion::new(sender), }), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, @@ -481,3 +499,7 @@ impl Drop for PromiseAwareFn { } } } + +#[cfg(test)] +#[path = "../tests/rust/promise_call_tests.rs"] +mod tests; diff --git a/crates/node/tests/callback_error_tests.mjs b/crates/node/tests/callback_error_tests.mjs index c05eccef9..142697f80 100644 --- a/crates/node/tests/callback_error_tests.mjs +++ b/crates/node/tests/callback_error_tests.mjs @@ -141,4 +141,17 @@ describe('callback error helpers', () => { /PromiseAwareFn threadsafe function closed/i, ); }); + + it('PromiseAwareFn argument conversion failures reject without invoking the callback', async () => { + let invoked = false; + await assert.rejects( + () => + __testClosedPromiseAwareCall(() => { + invoked = true; + return null; + }, true), + /forced PromiseAwareFn conversion failure/i, + ); + assert.equal(invoked, false); + }); }); diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 31a64fc84..c1d62acf7 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -2,18 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import assert from 'node:assert/strict'; -import { execFile } from 'node:child_process'; import { describe, it } from 'node:test'; import { createRequire } from 'node:module'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { promisify } from 'node:util'; const require = createRequire(import.meta.url); const lib = require('../index.js'); const plugin = require('../plugin.js'); -const execFileAsync = promisify(execFile); function capture(name) { const events = []; @@ -132,48 +129,6 @@ describe('event sanitizer registries', () => { assert.deepEqual(events.at(-1).data, { sanitized: true }); }); - it('keeps synchronous exporter flush and shutdown reentrant inside Promise sanitizers', async () => { - const addonPath = require.resolve('../index.js'); - const runExporterScenario = async (kind) => { - const script = String.raw` - const fs = require('node:fs'); - const os = require('node:os'); - const path = require('node:path'); - const lib = require(${JSON.stringify(addonPath)}); - const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'nemo-relay-reentrant-exporter-')); - const exporter = ${kind === 'atof' - ? "new lib.AtofExporter({ outputDirectory: directory })" - : "new lib.OpenTelemetrySubscriber({ type: 'full', endpoint: 'http://127.0.0.1:9', timeoutMillis: 10 })"}; - const sanitizerName = ${JSON.stringify(`node-reentrant-${kind}-sanitizer`)}; - const subscriberName = ${JSON.stringify(`node-reentrant-${kind}-subscriber`)}; - lib.registerSubscriber(subscriberName, () => {}); - lib.registerMarkSanitizeGuardrail(sanitizerName, 0, async (_event, fields) => { - await new Promise((resolve) => setImmediate(resolve)); - exporter.forceFlush(); - exporter.shutdown(); - return fields; - }); - lib.event(${JSON.stringify(`node-reentrant-${kind}-event`)}); - lib.flushSubscribers().then(() => { - lib.deregisterMarkSanitizeGuardrail(sanitizerName); - lib.deregisterSubscriber(subscriberName); - fs.rmSync(directory, { recursive: true, force: true }); - process.stdout.write('ok'); - }, (error) => { - process.stderr.write(String(error?.stack ?? error)); - process.exitCode = 1; - }); - `; - const { stdout } = await execFileAsync(process.execPath, ['--eval', script], { - timeout: 10_000, - }); - assert.equal(stdout, 'ok'); - }; - - await runExporterScenario('atof'); - await runExporterScenario('otel'); - }); - it('publishes nested Promise sanitizer events before already queued events', async () => { const events = capture('node-event-sanitize-nested-order-sub'); let sanitizerEntered; @@ -184,18 +139,14 @@ describe('event sanitizer registries', () => { const release = new Promise((resolve) => { releaseSanitizer = resolve; }); - lib.registerMarkSanitizeGuardrail( - 'node-event-sanitize-nested-order', - 0, - async (event, fields) => { - if (event.name === 'node-outer-event') { - sanitizerEntered(); - await release; - lib.withScopeStack(lib.createScopeStack(), () => lib.event('node-nested-event')); - } - return fields; - }, - ); + lib.registerMarkSanitizeGuardrail('node-event-sanitize-nested-order', 0, async (event, fields) => { + if (event.name === 'node-outer-event') { + sanitizerEntered(); + await release; + lib.withScopeStack(lib.createScopeStack(), () => lib.event('node-nested-event')); + } + return fields; + }); try { lib.event('node-outer-event'); await entered; @@ -242,9 +193,7 @@ describe('event sanitizer registries', () => { await release; observedParents.push(lib.getHandle().uuid); lib.event('scope-context-nested', null, { originalParent: event.parent_uuid }); - observedOverrides.push( - lib.withScopeStack(overrideStack, () => lib.getHandle().uuid), - ); + observedOverrides.push(lib.withScopeStack(overrideStack, () => lib.getHandle().uuid)); lib.setThreadScopeStack(overrideStack); observedOverrides.push(lib.getHandle().uuid); return fields; @@ -283,16 +232,12 @@ describe('event sanitizer registries', () => { it('preserves the ending scope across an async scope-end sanitizer', async () => { const events = capture('node-scope-end-context-sub'); const observed = []; - lib.registerScopeSanitizeEndGuardrail( - 'node-scope-end-context', - 0, - async (_event, fields) => { - observed.push(lib.getHandle().uuid); - await new Promise((resolve) => setImmediate(resolve)); - observed.push(lib.getHandle().uuid); - return fields; - }, - ); + lib.registerScopeSanitizeEndGuardrail('node-scope-end-context', 0, async (_event, fields) => { + observed.push(lib.getHandle().uuid); + await new Promise((resolve) => setImmediate(resolve)); + observed.push(lib.getHandle().uuid); + return fields; + }); const scope = lib.pushScope('node-ending-scope', lib.ScopeType.Agent); try { lib.popScope(scope); @@ -339,82 +284,7 @@ describe('event sanitizer registries', () => { assert.deepEqual(events.at(-1).data, { snapshotted: true }); }); - it('does not deadlock when an async sanitizer flushes subscribers', async () => { - const events = capture('node-event-sanitize-reentrant-flush-sub'); - let flushReturned = false; - lib.registerMarkSanitizeGuardrail('node-event-reentrant-flush', 0, async (_event, fields) => { - await lib.flushSubscribers(); - flushReturned = true; - return fields; - }); - try { - lib.event('reentrant-flush-checkpoint', null, { raw: true }); - await lib.flushSubscribers(); - await waitFor(events, 1); - } finally { - lib.deregisterMarkSanitizeGuardrail('node-event-reentrant-flush'); - lib.deregisterSubscriber('node-event-sanitize-reentrant-flush-sub'); - } - assert.equal(flushReturned, true); - }); - - it('preserves sanitizer re-entrancy through nested managed middleware', async () => { - const events = capture('node-event-sanitize-nested-middleware-sub'); - const middlewareFlushes = []; - lib.registerToolConditionalExecutionGuardrail( - 'node-event-nested-middleware-conditional', - 0, - async (name) => { - if (name === 'node-event-nested-middleware-tool') { - await lib.flushSubscribers(); - middlewareFlushes.push('conditional'); - } - return null; - }, - ); - lib.registerToolExecutionIntercept( - 'node-event-nested-middleware-outer', - 0, - async (args, next) => ({ result: await next(args) }), - ); - lib.registerToolExecutionIntercept( - 'node-event-nested-middleware-inner', - 10, - async (args, next) => { - await lib.flushSubscribers(); - middlewareFlushes.push('execution'); - return { result: await next(args) }; - }, - ); - lib.registerMarkSanitizeGuardrail( - 'node-event-nested-middleware-sanitizer', - 0, - async (event, fields) => { - if (event.name === 'nested-middleware-checkpoint') { - await lib.toolCallExecute('node-event-nested-middleware-tool', {}, (args) => args); - } - return fields; - }, - ); - try { - lib.event('nested-middleware-checkpoint', null, { raw: true }); - const state = await Promise.race([ - lib.flushSubscribers().then(() => 'flushed'), - new Promise((resolve) => setTimeout(() => resolve('blocked'), 500)), - ]); - assert.equal(state, 'flushed'); - await waitFor(events, 3); - } finally { - lib.deregisterMarkSanitizeGuardrail('node-event-nested-middleware-sanitizer'); - lib.deregisterToolConditionalExecutionGuardrail('node-event-nested-middleware-conditional'); - lib.deregisterToolExecutionIntercept('node-event-nested-middleware-outer'); - lib.deregisterToolExecutionIntercept('node-event-nested-middleware-inner'); - lib.deregisterSubscriber('node-event-sanitize-nested-middleware-sub'); - } - assert.deepEqual(middlewareFlushes, ['conditional', 'execution']); - }); - - it('does not treat an unrelated flush as sanitizer re-entrancy', async () => { + it('waits for an in-flight sanitizer when flushed externally', async () => { const events = capture('node-event-sanitize-independent-flush-sub'); let releaseSanitizer; let sanitizerEntered; @@ -449,7 +319,7 @@ describe('event sanitizer registries', () => { }); it('queues managed event sanitizers without blocking execution', async () => { - lib.registerSubscriber('node-event-inline-flush-sub', () => {}); + lib.registerSubscriber('node-event-queued-managed-sub', () => {}); let blockerEntered; const entered = new Promise((resolve) => { blockerEntered = resolve; @@ -458,156 +328,36 @@ describe('event sanitizer registries', () => { const release = new Promise((resolve) => { releaseBlocker = resolve; }); - let inlineFlushReturned = false; + let inlineSanitizerReturned = false; - lib.registerMarkSanitizeGuardrail('node-event-inline-flush-blocker', 0, async (_event, fields) => { + lib.registerMarkSanitizeGuardrail('node-event-queued-managed-blocker', 0, async (_event, fields) => { blockerEntered(); await release; return fields; }); - lib.registerScopeSanitizeStartGuardrail('node-event-inline-flush', 0, async (_event, fields) => { - await lib.flushSubscribers(); - inlineFlushReturned = true; + lib.registerScopeSanitizeStartGuardrail('node-event-queued-managed', 0, async (_event, fields) => { + inlineSanitizerReturned = true; return fields; }); try { - lib.event('inline-flush-blocker', null, { raw: true }); + lib.event('queued-managed-blocker', null, { raw: true }); await entered; - const execution = lib.toolCallExecute('inline-flush-tool', {}, (args) => args); + const execution = lib.toolCallExecute('queued-managed-tool', {}, (args) => args); const executionState = await Promise.race([ execution.then(() => 'executed'), new Promise((resolve) => setTimeout(() => resolve('blocked'), 250)), ]); assert.equal(executionState, 'executed'); - assert.equal(inlineFlushReturned, false); + assert.equal(inlineSanitizerReturned, false); releaseBlocker(); await lib.flushSubscribers(); - assert.equal(inlineFlushReturned, true); + assert.equal(inlineSanitizerReturned, true); } finally { releaseBlocker(); - lib.deregisterMarkSanitizeGuardrail('node-event-inline-flush-blocker'); - lib.deregisterScopeSanitizeStartGuardrail('node-event-inline-flush'); - lib.deregisterSubscriber('node-event-inline-flush-sub'); - } - }); - - it('clears sanitizer re-entrancy in async descendants after settlement', async () => { - const events = capture('node-event-sanitize-descendant-flush-sub'); - const nestedStack = lib.createScopeStack(); - let secondSanitizerEntered; - const secondEntered = new Promise((resolve) => { - secondSanitizerEntered = resolve; - }); - let releaseSecondSanitizer; - const releaseSecond = new Promise((resolve) => { - releaseSecondSanitizer = resolve; - }); - let descendantFlushStarted; - const flushStarted = new Promise((resolve) => { - descendantFlushStarted = resolve; - }); - let descendantFlush; - const flushed = new Promise((resolve, reject) => { - descendantFlush = { resolve, reject }; - }); - lib.registerMarkSanitizeGuardrail('node-event-descendant-flush', 0, async (event, fields) => { - if (event.name === 'descendant-flush-origin') { - lib.withScopeStack(nestedStack, () => { - setTimeout(async () => { - await secondEntered; - lib.flushSubscribers().then(descendantFlush.resolve, descendantFlush.reject); - descendantFlushStarted(); - }, 0); - }); - } else if (event.name === 'descendant-flush-blocked') { - secondSanitizerEntered(); - await releaseSecond; - } - return fields; - }); - try { - lib.event('descendant-flush-origin', null, { raw: true }); - lib.event('descendant-flush-blocked', null, { raw: true }); - await secondEntered; - await flushStarted; - const state = await Promise.race([ - flushed.then(() => 'flushed'), - new Promise((resolve) => setTimeout(() => resolve('pending'), 50)), - ]); - assert.equal(state, 'pending'); - releaseSecondSanitizer(); - await flushed; - await waitFor(events, 2); - } finally { - releaseSecondSanitizer(); - lib.deregisterMarkSanitizeGuardrail('node-event-descendant-flush'); - lib.deregisterSubscriber('node-event-sanitize-descendant-flush-sub'); - } - }); - - it('clears sanitizer re-entrancy after a native descendant round trip', async () => { - const events = capture('node-event-sanitize-native-descendant-sub'); - let secondSanitizerEntered; - const secondEntered = new Promise((resolve) => { - secondSanitizerEntered = resolve; - }); - let releaseSecondSanitizer; - const releaseSecond = new Promise((resolve) => { - releaseSecondSanitizer = resolve; - }); - let descendantFlushStarted; - const flushStarted = new Promise((resolve) => { - descendantFlushStarted = resolve; - }); - let descendantFlush; - let descendantExecution; - - lib.registerToolConditionalExecutionGuardrail( - 'node-event-native-descendant-conditional', - 0, - async (name) => { - if (name === 'node-event-native-descendant-tool') { - await secondEntered; - descendantFlush = lib.flushSubscribers(); - descendantFlushStarted(); - await descendantFlush; - } - return null; - }, - ); - lib.registerMarkSanitizeGuardrail( - 'node-event-native-descendant-sanitizer', - 0, - async (event, fields) => { - if (event.name === 'native-descendant-origin') { - descendantExecution = lib.toolCallExecute('node-event-native-descendant-tool', {}, (args) => args); - } else if (event.name === 'native-descendant-blocked') { - secondSanitizerEntered(); - await releaseSecond; - } - return fields; - }, - ); - try { - lib.event('native-descendant-origin', null, { raw: true }); - lib.event('native-descendant-blocked', null, { raw: true }); - await secondEntered; - await flushStarted; - const state = await Promise.race([ - descendantFlush.then(() => 'flushed'), - new Promise((resolve) => setTimeout(() => resolve('pending'), 50)), - ]); - assert.equal(state, 'pending'); - releaseSecondSanitizer(); - await descendantExecution; - await lib.flushSubscribers(); - await waitFor(events, 4); - } finally { - releaseSecondSanitizer(); - lib.deregisterMarkSanitizeGuardrail('node-event-native-descendant-sanitizer'); - lib.deregisterToolConditionalExecutionGuardrail('node-event-native-descendant-conditional'); - lib.deregisterSubscriber('node-event-sanitize-native-descendant-sub'); + lib.deregisterMarkSanitizeGuardrail('node-event-queued-managed-blocker'); + lib.deregisterScopeSanitizeStartGuardrail('node-event-queued-managed'); + lib.deregisterSubscriber('node-event-queued-managed-sub'); } }); diff --git a/crates/node/tests/rust/promise_call_tests.rs b/crates/node/tests/rust/promise_call_tests.rs new file mode 100644 index 000000000..e51851084 --- /dev/null +++ b/crates/node/tests/rust/promise_call_tests.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; + +#[test] +fn execution_next_context_restores_scope() { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + let scope_stack = nemo_relay::api::runtime::create_scope_stack(); + let context = nemo_relay::api::runtime::with_scope_stack(scope_stack.clone(), || { + MiddlewareContinuationContext::capture() + }); + + let observed = + tokio::spawn(async move { context.run(async move { current_scope_stack() }).await }) + .await + .unwrap(); + + assert!(Arc::ptr_eq(&observed, &scope_stack)); + }); +} diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 817af4b4f..94288af05 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -885,7 +885,12 @@ pub type NemoRelayNativeAsyncNextStreamCb = unsafe extern "C" fn( /// /// The callback owns `next` and `stream` and must release each exactly once. /// It may push chunks before returning or retain the handles and return -/// `Pending`; no implicit timeout is applied. +/// `Pending`; no implicit timeout is applied. Relay can invoke separate +/// middleware calls concurrently without stable OS-thread affinity. Retained +/// handles may be used from a plugin-owned thread, while callbacks supplied to +/// `async_next_invoke_stream` run on a Relay runtime worker. The plugin must +/// synchronize shared `user_data` and callback state and serialize each +/// handle's final release after its last operation returns. pub type NemoRelayNativeAsyncStreamMiddlewareCb = unsafe extern "C" fn( user_data: *mut c_void, invocation_json: *const NemoRelayNativeString, @@ -903,7 +908,13 @@ pub type NemoRelayNativeAsyncStreamMiddlewareCb = unsafe extern "C" fn( /// the invocation and must call `async_next_release` exactly once after its /// final use, regardless of whether it returns `Complete` or `Pending`. The /// host never reclaims a `next` handle after handing it to the callback. -/// `next` is null for non-execution middleware. +/// `next` is null for non-execution middleware. Relay invokes the callback on +/// the Tokio runtime worker polling that middleware invocation, without stable +/// OS-thread affinity; separate invocations may run concurrently. After +/// returning `Pending`, retained completion and `next` handles may be used from +/// a plugin-owned thread. The plugin must synchronize shared `user_data` and +/// callback state and serialize each handle's final release after its last +/// operation returns. pub type NemoRelayNativeAsyncMiddlewareCb = unsafe extern "C" fn( user_data: *mut c_void, invocation_json: *const NemoRelayNativeString, diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index fb6360293..5ca8097fc 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1564,9 +1564,10 @@ fn deregister_subscriber(name: &str) -> PyResult { /// Wait for subscriber callbacks queued before this call to finish. /// -/// Public Python wrappers prevent re-entrant event-sanitizer callbacks from -/// waiting on the serial dispatcher. Publication middleware must not move such -/// a re-entrant flush to an unmarked worker thread. +/// Call this function outside subscribers, event sanitizers, conditional +/// guardrails, and request or execution intercepts. The public Python wrapper +/// lets queued tool and LLM observability sanitizers return without waiting on +/// their own publication. #[pyfunction] fn flush_subscribers(py: Python<'_>) -> PyResult<()> { py.detach(core_subscriber_api::flush_subscribers) diff --git a/crates/python/src/py_callable.rs b/crates/python/src/py_callable.rs index 0c2aa674e..2d0189b9d 100644 --- a/crates/python/src/py_callable.rs +++ b/crates/python/src/py_callable.rs @@ -32,8 +32,8 @@ use nemo_relay::api::runtime::{ EventSanitizeFn, EventSubscriberFn, LlmConditionalFn, LlmExecutionNextFn, LlmJsonStream, LlmRequestInterceptFn, LlmSanitizeRequestContext, LlmSanitizeRequestFn, LlmSanitizeResponseContext, LlmSanitizeResponseFn, LlmStreamExecutionNextFn, LlmStreamInner, - ToolConditionalFn, ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn, current_scope_stack, - snapshot_scope_stack, + MiddlewareContinuationContext, ToolConditionalFn, ToolExecutionNextFn, ToolInterceptFn, + ToolSanitizeFn, current_scope_stack, }; use nemo_relay::error::{FlowError, Result as FlowResult}; use pyo3::exceptions::PyRuntimeError; @@ -425,8 +425,10 @@ fn copy_publication_invocation_with_buffer<'py>( publication_buffer: Option, ) -> PyResult<(Bound<'py, PyAny>, Option)> { let invocation_context = context.context.bind(py).call_method0("copy")?; - let scope_stack = snapshot_scope_stack(¤t_scope_stack()) - .map_err(|error| PyRuntimeError::new_err(error.to_string()))?; + // The dispatcher already installs an isolated emission-time snapshot. + // Retain that handle so callback-local scope mutations stay visible to + // nested events without exposing stack cloning as a public runtime API. + let scope_stack = current_scope_stack(); let scope_stack = Py::new( py, PyScopeStack { @@ -947,6 +949,7 @@ pub fn wrap_py_tool_exec_fn( #[pyclass] struct PyToolNextFn { inner: ToolExecutionNextFn, + context: MiddlewareContinuationContext, } #[pymethods] @@ -957,10 +960,11 @@ impl PyToolNextFn { args: &Bound<'py, PyAny>, ) -> PyResult> { let next = self.inner.clone(); + let context = self.context.clone(); let json_args = py_to_json(args)?; - let future = next(json_args); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let result = future + let result = context + .invoke(move || next(json_args)) .await .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; Python::attach(|py| json_to_py(py, &result)) @@ -973,15 +977,17 @@ impl PyToolNextFn { #[pyclass] struct PyLlmNextFn { inner: LlmExecutionNextFn, + context: MiddlewareContinuationContext, } #[pymethods] impl PyLlmNextFn { fn __call__<'py>(&self, py: Python<'py>, request: PyLLMRequest) -> PyResult> { let next = self.inner.clone(); - let future = next(request.inner); + let context = self.context.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let result = future + let result = context + .invoke(move || next(request.inner)) .await .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; Python::attach(|py| json_to_py(py, &result)) @@ -994,15 +1000,17 @@ impl PyLlmNextFn { #[pyclass] struct PyLlmStreamNextFn { inner: LlmStreamExecutionNextFn, + context: MiddlewareContinuationContext, } #[pymethods] impl PyLlmStreamNextFn { fn __call__<'py>(&self, py: Python<'py>, request: PyLLMRequest) -> PyResult> { let next = self.inner.clone(); - let future = next(request.inner); + let context = self.context.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let rust_stream = future + let rust_stream = context + .invoke(move || next(request.inner)) .await .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; @@ -1010,12 +1018,16 @@ impl PyLlmStreamNextFn { let (tx, rx) = tokio::sync::mpsc::channel::>(32); let (cancel, cancel_rx) = tokio::sync::watch::channel(false); let (closed, closed_rx) = tokio::sync::watch::channel(None); - tokio::spawn(crate::py_api::forward_stream_to_channel( - rust_stream, - tx, - cancel_rx, - closed, - )); + tokio::spawn(async move { + context + .run(crate::py_api::forward_stream_to_channel( + rust_stream, + tx, + cancel_rx, + closed, + )) + .await; + }); Ok(crate::py_types::PyLlmStream { receiver: Arc::new(tokio::sync::Mutex::new(rx)), @@ -1043,7 +1055,10 @@ pub fn wrap_py_tool_exec_intercept_fn( .map_err(|error| FlowError::Internal(error.to_string()))?; let py_args = json_to_py(py, &args).map_err(|e: PyErr| FlowError::Internal(e.to_string()))?; - let py_next = PyToolNextFn { inner: next }; + let py_next = PyToolNextFn { + inner: next, + context: MiddlewareContinuationContext::capture(), + }; let py_next = py_next .into_pyobject(py) .map_err(|e| FlowError::Internal(e.to_string()))? @@ -1105,7 +1120,10 @@ pub fn wrap_py_llm_exec_intercept_fn( copy_middleware_invocation(py, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; let py_req = PyLLMRequest { inner: request }; - let py_next = PyLlmNextFn { inner: next }; + let py_next = PyLlmNextFn { + inner: next, + context: MiddlewareContinuationContext::capture(), + }; let py_req = py_req .into_pyobject(py) .map_err(|e| FlowError::Internal(e.to_string()))? @@ -1170,7 +1188,10 @@ pub fn wrap_py_llm_stream_exec_intercept_fn( copy_middleware_invocation(py, task_locals) .map_err(|error| FlowError::Internal(error.to_string()))?; let py_req = PyLLMRequest { inner: request }; - let py_next = PyLlmStreamNextFn { inner: next }; + let py_next = PyLlmStreamNextFn { + inner: next, + context: MiddlewareContinuationContext::capture(), + }; let py_req = py_req .into_pyobject(py) .map_err(|e: PyErr| FlowError::Internal(e.to_string()))? diff --git a/crates/python/src/py_types/observability.rs b/crates/python/src/py_types/observability.rs index 21bd52e26..b5364acd8 100644 --- a/crates/python/src/py_types/observability.rs +++ b/crates/python/src/py_types/observability.rs @@ -376,19 +376,17 @@ impl PyAtofExporter { .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) } - /// Outside a native subscriber callback, wait for queued subscriber delivery, then flush the - /// file sink or ask the stream sink to drain for up to its timeout. A re-entrant call does not - /// establish the delivery barrier. A stream timeout is logged and does not by itself return an - /// error. + /// Outside subscriber and middleware callbacks, wait for queued subscriber delivery, then + /// flush the file sink or ask the stream sink to drain for up to its timeout. A stream timeout + /// is logged and does not by itself return an error. pub(crate) fn force_flush(&self, py: Python<'_>) -> PyResult<()> { py.detach(|| self.inner.force_flush()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) } - /// Outside a native subscriber callback, wait for queued subscriber delivery, then flush the - /// file sink or ask the stream sink to drain and close up to its timeout. A re-entrant call - /// does not establish the delivery barrier. A stream timeout is logged and does not by itself - /// return an error. + /// Outside subscriber and middleware callbacks, wait for queued subscriber delivery, then + /// flush the file sink or ask the stream sink to drain and close up to its timeout. A stream + /// timeout is logged and does not by itself return an error. pub(crate) fn shutdown(&self, py: Python<'_>) -> PyResult<()> { py.detach(|| self.inner.shutdown()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) diff --git a/crates/python/tests/coverage/py_callable_coverage_tests.rs b/crates/python/tests/coverage/py_callable_coverage_tests.rs index df5b86afc..cac874dbe 100644 --- a/crates/python/tests/coverage/py_callable_coverage_tests.rs +++ b/crates/python/tests/coverage/py_callable_coverage_tests.rs @@ -579,8 +579,10 @@ async def collect_stream(awaitable): with_event_loop(py, |event_loop| { pyo3_async_runtimes::tokio::run_until_complete(event_loop, async move { + let continuation_context = MiddlewareContinuationContext::capture(); let tool_next = PyToolNextFn { inner: Arc::new(|args| Box::pin(async move { Ok(json!({"echo": args["x"]})) })), + context: continuation_context.clone(), }; let tool_awaitable = Python::attach(|py| { tool_next.__call__(py, tool_args.bind(py)).unwrap().unbind() @@ -601,6 +603,7 @@ async def collect_stream(awaitable): inner: Arc::new(|_| { Box::pin(async { Err(FlowError::Internal("tool next boom".into())) }) }), + context: continuation_context.clone(), }; let tool_err_awaitable = Python::attach(|py| { tool_next_err @@ -626,6 +629,7 @@ async def collect_stream(awaitable): inner: Arc::new(|request| { Box::pin(async move { Ok(json!({"model": request.content["model"]})) }) }), + context: continuation_context.clone(), }; let llm_awaitable = Python::attach(|py| { llm_next @@ -652,6 +656,7 @@ async def collect_stream(awaitable): inner: Arc::new(|_| { Box::pin(async { Err(FlowError::Internal("llm next boom".into())) }) }), + context: continuation_context.clone(), }; let llm_err_awaitable = Python::attach(|py| { llm_next_err @@ -686,6 +691,7 @@ async def collect_stream(awaitable): )) }) }), + context: continuation_context.clone(), }; let stream_awaitable = Python::attach(|py| { stream_next @@ -714,6 +720,7 @@ async def collect_stream(awaitable): inner: Arc::new(|_| { Box::pin(async { Err(FlowError::Internal("stream next boom".into())) }) }), + context: continuation_context, }; let stream_err_awaitable = Python::attach(|py| { stream_next_err @@ -906,3 +913,21 @@ def llm_custom_awaitable(request): let runtime = tokio::runtime::Runtime::new().unwrap(); assert_eq!(runtime.block_on(llm_custom(make_request())).unwrap(), None); } + +#[test] +fn execution_next_context_restores_scope() { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + let scope_stack = nemo_relay::api::runtime::create_scope_stack(); + let context = nemo_relay::api::runtime::with_scope_stack(scope_stack.clone(), || { + MiddlewareContinuationContext::capture() + }); + + let observed = + tokio::spawn(async move { context.run(async move { current_scope_stack() }).await }) + .await + .unwrap(); + + assert!(Arc::ptr_eq(&observed, &scope_stack)); + }); +} diff --git a/docs/about-nemo-relay/concepts/middleware.mdx b/docs/about-nemo-relay/concepts/middleware.mdx index ee2e72638..55bdad16f 100644 --- a/docs/about-nemo-relay/concepts/middleware.mdx +++ b/docs/about-nemo-relay/concepts/middleware.mdx @@ -37,6 +37,21 @@ APIs (`tool_call`, `tool_call_end`, `llm_call`, and `llm_call_end`) remain synchronous: they create or close their handle immediately and queue observability work rather than awaiting it. + +Event sanitizers, conditional-execution guardrails, request intercepts, and +execution intercepts are not re-entrant. These callbacks must not invoke +another NeMo Relay API that runs middleware, flushes subscriber delivery, waits +on an exporter, or clears plugins. Scope APIs remain supported: callbacks may +create, push, or pop scopes at any nesting level and may replace the active +scope stack with an arbitrary stack. Emitting a new event is the only +supported operation that can enqueue additional callback work; Relay queues +that event for later publication instead of recursively dispatching it. + +An execution intercept may invoke the `next` continuation supplied to that +callback. This is the only supported way for an intercept to enter the +remaining execution chain. Call `next` only as documented for that intercept. + + ## Registration Levels Middleware and subscribers can be registered at different levels depending on their diff --git a/docs/about-nemo-relay/concepts/subscribers.mdx b/docs/about-nemo-relay/concepts/subscribers.mdx index d24032e39..d04ff4c2e 100644 --- a/docs/about-nemo-relay/concepts/subscribers.mdx +++ b/docs/about-nemo-relay/concepts/subscribers.mdx @@ -130,6 +130,16 @@ shape for their target backend. Native subscribers are invoked by one process-wide worker thread in FIFO event order and subscriber snapshot order. + +Subscriber callbacks are not re-entrant. Do not invoke a NeMo Relay API that +runs middleware, flushes subscriber delivery, waits on an exporter, or clears +plugins from a subscriber callback. Scope APIs remain supported: callbacks may +create, push, or pop scopes at any nesting level and may replace the active +scope stack with an arbitrary stack. Emitting a new event is the only +supported operation that can enqueue additional callback work; Relay queues +that event behind the active publication instead of recursively delivering it. + + ## Waiting for Delivery An event-producing call returning is not a delivery barrier. These guarantees @@ -162,10 +172,14 @@ exporter-specific flush, export, or shutdown operation described in Do not invoke a subscriber flush, an exporter barrier, or plugin clear from a -native subscriber callback. To avoid blocking its worker, the native dispatcher -returns a re-entrant subscriber flush without waiting. Callbacks later in the -active dispatch snapshot can still run after the barrier returns. Run those -operations after the callback returns. +subscriber, event-sanitizer, conditional-guardrail, request-intercept, or +execution-intercept callback. These operations are not supported there and do +not establish a valid delivery barrier; depending on the binding or exporter, +they can also create a wait cycle. Run them after the callback returns. + +Queued tool and LLM observability sanitizers are a narrow exception: Python and +Node.js permit a subscriber flush call there, but it returns without waiting +for the sanitizer's own publication. If the process terminates before subscriber delivery and exporter teardown complete, queued telemetry can be lost. Returning from the event-producing API diff --git a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx index 1429f47c0..f3a7d19e8 100644 --- a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx +++ b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx @@ -144,6 +144,20 @@ it exactly once, call `async_completion_release`, and release an async `next` handle after use. The host marks a completion cancelled when the awaiting runtime work is dropped; late and duplicate settlement is rejected safely. +Relay invokes an asynchronous middleware callback synchronously on the Tokio +runtime worker that is polling that middleware invocation. There is no stable +OS-thread affinity, and separate invocations can run concurrently. After +returning `Pending`, a plugin can use its retained completion, stream, or +`next` handle from another plugin-owned thread. The host synchronizes those +opaque handles, schedules `next` on the captured Relay runtime, and can invoke +an incremental downstream-stream callback on a Relay runtime worker. The +plugin must synchronize shared `user_data` and callback state, and must keep +them alive until the corresponding host-owned registration and all +callback-owned handle references are released. Do not race a handle's release +operation against settlement, cancellation inspection, stream operations, or +`next` invocation using that same reference; serialize the final release after +the last such call returns. + Event sanitizers registered through this extension still run on Relay's serial publication dispatcher. Scope and mark emission remain synchronous and their sanitized events are delivered later in emission order. @@ -211,9 +225,9 @@ JSON string handles, which callers release with the ordinary host string release operation. A successful null sanitizer output omits the LLM observability payload and its -annotation. Returning an error also fails closed rather than exposing the -unsanitized payload. Neither case changes the client-visible request or -response. +annotation. Returning an error fails open: Relay records the callback error and +preserves the last valid observability payload and annotation. Neither case +changes the client-visible request or response. Use the `nemo-relay-plugin` crate rather than the host `nemo-relay` runtime crate. Refer to [Build a Rust Native Plugin](/build-plugins/dynamic-plugins/native-dynamic/rust-native-plugin-example) diff --git a/docs/getting-started/quick-start/nodejs.mdx b/docs/getting-started/quick-start/nodejs.mdx index 22f205344..bc560202d 100644 --- a/docs/getting-started/quick-start/nodejs.mdx +++ b/docs/getting-started/quick-start/nodejs.mdx @@ -86,7 +86,7 @@ async function main() { console.log(llmResult); }); - flushSubscribers(); + await flushSubscribers(); await new Promise((resolve) => setImmediate(resolve)); deregisterSubscriber("quickstart-printer"); } diff --git a/docs/instrument-applications/adding-scopes-and-marks.mdx b/docs/instrument-applications/adding-scopes-and-marks.mdx index d40270f0f..5f37cb00d 100644 --- a/docs/instrument-applications/adding-scopes-and-marks.mdx +++ b/docs/instrument-applications/adding-scopes-and-marks.mdx @@ -101,7 +101,7 @@ async function main() { { request_id: "req-123" }, ); } finally { - flushSubscribers(); + await flushSubscribers(); await new Promise((resolve) => setImmediate(resolve)); deregisterSubscriber("scope-check"); } @@ -187,9 +187,8 @@ Check that the subscriber prints: - One mark event for `planning-finished` - One scope end event for `agent-run` -Native subscribers are delivered asynchronously. Flush subscribers before -validating printed or captured output. In Node.js, also wait one event-loop tick -after `flushSubscribers()` so queued JavaScript callbacks can run. +Native subscribers are delivered asynchronously. Await `flushSubscribers()` +and then one event-loop turn before validating printed or captured output. If marks appear outside the intended trace, pass the active scope handle explicitly or make sure the mark is emitted while the scope is active. diff --git a/docs/instrument-applications/instrument-llm-call.mdx b/docs/instrument-applications/instrument-llm-call.mdx index 2ed38dc4f..3b76dd709 100644 --- a/docs/instrument-applications/instrument-llm-call.mdx +++ b/docs/instrument-applications/instrument-llm-call.mdx @@ -147,7 +147,7 @@ async function main() { console.log(result); }); } finally { - flushSubscribers(); + await flushSubscribers(); await new Promise((resolve) => setImmediate(resolve)); deregisterSubscriber("llm-check"); } @@ -234,9 +234,8 @@ Check both behavior and instrumentation: `annotated_response.usage.cost` only when a response codec decoded model and usage fields and a source matched the model. -Native subscriber delivery is asynchronous. Flush subscribers before validating -printed output. In Node.js, also wait one event-loop tick after -`flushSubscribers()` so JavaScript callbacks can run. +Native subscriber delivery is asynchronous. Await `flushSubscribers()` and +then one event-loop turn before validating printed output. - LLM start input contains the request after request intercepts and sanitize-request guardrails. Each owning agent scope starts fresh, and a diff --git a/docs/instrument-applications/instrument-tool-call.mdx b/docs/instrument-applications/instrument-tool-call.mdx index e9dbad4f8..f20313c7c 100644 --- a/docs/instrument-applications/instrument-tool-call.mdx +++ b/docs/instrument-applications/instrument-tool-call.mdx @@ -116,7 +116,7 @@ async function main() { console.log(result); }); } finally { - flushSubscribers(); + await flushSubscribers(); await new Promise((resolve) => setImmediate(resolve)); deregisterSubscriber("instrumentation-check"); } @@ -194,9 +194,8 @@ Check both behavior and instrumentation: - The subscriber prints an agent or request scope event. - The subscriber prints tool start and tool end events for `search`. -Native subscriber delivery is asynchronous. Flush subscribers before validating -printed output. In Node.js, also wait one event-loop tick after -`flushSubscribers()` so JavaScript callbacks can run. +Native subscriber delivery is asynchronous. Await `flushSubscribers()` and +then one event-loop turn before validating printed output. - Tool start input contains the request arguments after request intercepts and sanitize-request guardrails. - Tool end output contains the tool result after response guardrails. diff --git a/docs/reference/event-sanitizers.mdx b/docs/reference/event-sanitizers.mdx index ea69692fd..6a139aa07 100644 --- a/docs/reference/event-sanitizers.mdx +++ b/docs/reference/event-sanitizers.mdx @@ -55,6 +55,15 @@ the last valid observability payload without changing provider execution. In Nod a synchronous sanitizer callback that throws also fails open; Relay records the error for `getLastCallbackError()`. + +Event sanitizer callbacks are not re-entrant. Do not call another NeMo Relay +API that runs middleware, flushes subscribers, waits on an exporter, or clears +plugins. Scope APIs remain supported: callbacks may create, push, or pop scopes +at any nesting level and may replace the active scope stack with an arbitrary +stack. Emitting a new event is the only supported operation that can +enqueue additional callback work; Relay queues it for later publication. + + ## Async Delivery and Ordering Event sanitizer callbacks may be asynchronous: use an `async def` callback in @@ -197,28 +206,17 @@ activation fails. ## Experimental C and Go Bindings -The source-first C API retains `NemoRelayEventSanitizeCb` and adds parallel -completion-based async registration APIs. An async callback returns `Complete` -or `Pending` and settles its one-shot completion handle with resolve or reject. -The absence of an implicit timeout is intentional: Relay preserves strict FIFO -publication, so one unsettled `Pending` completion blocks every later event in -that publication queue. Plugin authors should arrange their own operation -deadline and settle each retained completion exactly once on every success, -failure, and cancellation path. - -Relay cancels the handle when its invocation is abandoned, which is the -host-supported recovery mechanism; late or duplicate settlement after -cancellation is rejected safely. After resolving or rejecting a retained -completion, call `nemo_relay_async_completion_release` to release the -callback-owned reference. Global names start with `nemo_relay_register_`, and -scope-local names start with `nemo_relay_scope_register_`. - -The Go binding provides `EventSanitizeFields`, `EventSanitizeFunc`, and -`AsyncMiddlewareFunc` variants for global and scope-local event sanitizers. -Async Go callbacks receive a `context.Context`; Relay cancels it when the -invocation is abandoned. Because a returned `EventSanitizeFields` replaces all -three fields, copy the supplied value and modify only the fields that should -change. +The source-first C API uses `NemoRelayEventSanitizeCb`. It provides global, +scope-local, and plugin-context registration functions for all three surfaces. +Global names start with `nemo_relay_register_`, and scope-local names start +with `nemo_relay_scope_register_`. + +The Go binding provides `EventSanitizeFields`, `EventSanitizeFunc`, global +`Register*SanitizeGuardrail` helpers, scope-local +`ScopeRegister*SanitizeGuardrail` helpers, and the same methods on +`PluginContext`. The `guardrails` package provides shorter aliases. Because a +returned `EventSanitizeFields` replaces all three fields, copy the supplied +value and modify only the fields that should change. ## Related Topics diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index 231d1b65c..48e3aad38 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -15,7 +15,7 @@ intervening release in sequence. NeMo Relay 0.7 makes the Rust middleware callback contract asynchronous and adds awaitable middleware support across the in-process bindings, native -plugins, raw C FFI consumers, and worker plugins. It also changes the LLM +plugins, and worker plugins. It also changes the LLM observability sanitizer contract. Complete the following migrations before you run existing middleware or a sanitizer with a 0.7 host. @@ -40,7 +40,7 @@ existing error behavior for its middleware family. | Rust | `Fn(...) -> Result` | `Fn(...) -> Pin> + Send>>` | | Python | Direct return value | Direct return value or awaitable | | Node.js | Direct return value | Direct return value or `Promise` | -| Go / raw C FFI | Synchronous callback | Existing synchronous callback, or the new `Async` / completion-based registration API | +| Go / raw C FFI | Synchronous callback | Synchronous callback | Python's standalone middleware helpers preserve their direct synchronous return when called without a running `asyncio` loop. In that mode, registered @@ -84,6 +84,17 @@ Python and Node.js registration names are unchanged. Mark a Python callback `async def`, or return a Promise from Node.js, only when it needs asynchronous work; existing direct-value callbacks remain supported. + +Do not introduce recursive middleware calls while converting callbacks to +async. Event sanitizers, conditional-execution guardrails, request intercepts, +execution intercepts, and subscribers must not call NeMo Relay APIs that run +middleware, flush subscribers, wait on exporters, or clear plugins. Within +these callbacks, scope operations remain supported at every nesting level, +including replacement of the active stack with an arbitrary scope stack. Event +emission is the only supported operation that can enqueue additional callback +work. An execution intercept may also invoke its supplied `next` continuation. + + When queued Python middleware has no live captured event loop—including when it was registered outside a loop or its registration loop has closed—the fallback uses `asyncio.run` with a fresh loop. Bare coroutine results are diff --git a/python/nemo_relay/_event_sanitizer_context.py b/python/nemo_relay/_event_sanitizer_context.py index 88ab27db2..0d108e1e1 100644 --- a/python/nemo_relay/_event_sanitizer_context.py +++ b/python/nemo_relay/_event_sanitizer_context.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Track re-entrant subscriber flushes from Python event sanitizers.""" +"""Track queued publication callbacks on their originating event loop.""" from __future__ import annotations @@ -12,7 +12,7 @@ class _CallbackState: - """Shared liveness for contexts copied from one sanitizer invocation.""" + """Shared liveness for contexts copied from one publication callback.""" __slots__ = ("active",) @@ -24,7 +24,7 @@ def __init__(self) -> None: def callback_active() -> bool: - """Return whether the current Python context is running an event sanitizer.""" + """Return whether the current context is running queued publication work.""" state = _ACTIVE.get() return state is not None and state.active @@ -69,7 +69,7 @@ async def async_iter_close(iterator: Any) -> None: def invoke(callback: Callable[..., Any], *args: Any) -> Any: - """Invoke a sanitizer while marking its sync and async execution contexts.""" + """Invoke queued publication work while marking its execution context.""" state = _ACTIVE.get() owner = state is None or not state.active if owner: diff --git a/python/nemo_relay/_native.pyi b/python/nemo_relay/_native.pyi index 3b516aca6..5b52de253 100644 --- a/python/nemo_relay/_native.pyi +++ b/python/nemo_relay/_native.pyi @@ -952,21 +952,19 @@ class AtofExporter: def force_flush(self) -> None: """Flush the exporter. - Outside a native subscriber callback, wait for queued subscriber - delivery, then flush the file sink or ask the stream sink to drain up - to its timeout. A re-entrant call does not establish the delivery - barrier. A stream timeout is logged and does not by itself return an - error. + Outside subscriber and middleware callbacks, wait for queued + subscriber delivery, then flush the file sink or ask the stream sink + to drain up to its timeout. A stream timeout is logged and does not by + itself return an error. """ ... def shutdown(self) -> None: """Flush the exporter and shut it down. - Outside a native subscriber callback, wait for queued subscriber - delivery, then flush the file sink or ask the stream sink to drain and - close up to its timeout. A re-entrant call does not establish the - delivery barrier. A stream timeout is logged and does not by itself - return an error. + Outside subscriber and middleware callbacks, wait for queued + subscriber delivery, then flush the file sink or ask the stream sink + to drain and close up to its timeout. A stream timeout is logged and + does not by itself return an error. """ ... @@ -1980,8 +1978,9 @@ def deregister_subscriber(name: str) -> bool: def flush_subscribers() -> None: """Wait for subscriber callbacks queued by native event emission. - Call this function outside subscriber callbacks. A re-entrant call returns - without waiting, so callbacks later in the same dispatch snapshot can run. + Call this function outside subscribers, event sanitizers, conditional + guardrails, and request or execution intercepts. The public Python wrapper + handles the limited queued tool/LLM observability-sanitizer exception. """ ... diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index 3e8e054e4..9f305fe38 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -28,7 +28,7 @@ def log_event(event): from collections.abc import Callable from typing import TYPE_CHECKING -from nemo_relay._event_sanitizer_context import callback_active as _event_sanitizer_callback_active +from nemo_relay._event_sanitizer_context import callback_active as _publication_callback_active from nemo_relay._native import ( deregister_subscriber as _native_deregister, ) @@ -193,16 +193,17 @@ def flush() -> None: waiting for observer work. Use this barrier in tests and shutdown paths when captured subscriber output must be complete before continuing. - Call this function outside subscriber and queued publication sanitizer - callbacks. A re-entrant call returns without waiting to avoid blocking the - dispatcher. Publication middleware must not move such a call to an unmarked - worker thread. From an ``asyncio`` task, await :func:`flush_async` instead. + Call this function outside subscribers, event sanitizers, conditional + guardrails, and request or execution intercepts. A queued tool or LLM + observability sanitizer may call it, but the call returns without waiting + for its own publication. From an ``asyncio`` task, await + :func:`flush_async` instead. Raises: RuntimeError: If called while an ``asyncio`` event loop is running on the current thread. """ - if _event_sanitizer_callback_active(): + if _publication_callback_active(): return None try: asyncio.get_running_loop() @@ -222,7 +223,7 @@ async def flush_async() -> None: thread coalesces concurrent barriers and waits for the native dispatcher without blocking the Python event loop. """ - if _event_sanitizer_callback_active(): + if _publication_callback_active(): return None loop = asyncio.get_running_loop() completed: asyncio.Future[None] = loop.create_future() diff --git a/python/tests/test_event_sanitizers.py b/python/tests/test_event_sanitizers.py index 547326ac1..0c6cf56fb 100644 --- a/python/tests/test_event_sanitizers.py +++ b/python/tests/test_event_sanitizers.py @@ -5,7 +5,6 @@ import asyncio import contextvars -import threading from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor from typing import cast @@ -14,7 +13,6 @@ import nemo_relay from nemo_relay import EventSanitizeFields, guardrails, plugin, scope, scope_local, subscribers -from nemo_relay._event_sanitizer_context import callback_active, loop_affine @pytest.fixture(name="capture_events") @@ -131,43 +129,6 @@ async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> Even ] -def test_sync_standalone_middleware_preserves_nested_publication_order(capture_events): - _capture_name, events = capture_events - entered = threading.Event() - release = threading.Event() - - def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: - if event.name == "python-sync-outer-event": - entered.set() - assert release.wait(timeout=2) - nemo_relay.tools.conditional_execution("python-nested-conditional", {}) - return fields - - guardrails.register_tool_conditional_execution( - "python-nested-conditional", - 0, - lambda _name, _args: None, - ) - guardrails.register_mark_sanitize("python-sync-nested-order", 0, sanitize) - try: - scope.event("python-sync-outer-event") - assert entered.wait(timeout=2) - scope.event("python-sync-later-event") - release.set() - subscribers.flush() - finally: - release.set() - guardrails.deregister_mark_sanitize("python-sync-nested-order") - guardrails.deregister_tool_conditional_execution("python-nested-conditional") - - assert [event.name for event in events] == [ - "python-sync-outer-event", - "python-nested-conditional", - "python-nested-conditional", - "python-sync-later-event", - ] - - async def test_scope_start_sanitizer_uses_started_scope_context(capture_events): _capture_name, events = capture_events observed_scope_uuids: list[str] = [] @@ -241,70 +202,6 @@ async def sanitize(_event: nemo_relay.Event, fields: EventSanitizeFields) -> Eve assert observed == ["emission", "emission"] -async def test_sanitizer_descendants_lose_reentrant_flush_after_settlement(capture_events): - blocker_entered = asyncio.Event() - release_blocker = asyncio.Event() - descendant_finished = asyncio.Event() - descendant_task: asyncio.Task[None] | None = None - - async def descendant() -> None: - await blocker_entered.wait() - await subscribers.flush_async() - descendant_finished.set() - - async def sanitize(event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: - nonlocal descendant_task - if event.name == "descendant-origin": - descendant_task = asyncio.create_task(descendant()) - elif event.name == "descendant-blocked": - blocker_entered.set() - await release_blocker.wait() - return fields - - guardrails.register_mark_sanitize("python-descendant-flush-liveness", 0, sanitize) - try: - scope.event("descendant-origin") - scope.event("descendant-blocked") - await asyncio.wait_for(blocker_entered.wait(), timeout=1) - await asyncio.sleep(0.05) - assert not descendant_finished.is_set() - release_blocker.set() - await asyncio.wait_for(subscribers.flush_async(), timeout=1) - assert descendant_task is not None - await asyncio.wait_for(descendant_task, timeout=1) - finally: - release_blocker.set() - guardrails.deregister_mark_sanitize("python-descendant-flush-liveness") - - -async def test_cancelled_sanitizer_expires_descendant_context(): - descendant_started = asyncio.Event() - release_descendant = asyncio.Event() - observed: list[bool] = [] - - async def descendant() -> None: - descendant_started.set() - await release_descendant.wait() - observed.append(callback_active()) - - async def never() -> None: - await asyncio.Event().wait() - - def sanitizer() -> object: - asyncio.create_task(descendant()) - return never() - - execution = asyncio.ensure_future(loop_affine(sanitizer, sanitizer=True)()) - await asyncio.wait_for(descendant_started.wait(), timeout=1) - execution.cancel() - with pytest.raises(asyncio.CancelledError): - await execution - release_descendant.set() - await asyncio.sleep(0) - - assert observed == [False] - - def test_sync_mark_sanitizer_uses_emitter_context(capture_events): request_id = contextvars.ContextVar("request_id", default="registration") observed: list[str] = [] @@ -436,36 +333,6 @@ async def register() -> None: assert observed == ["emission", "emission"] -@pytest.mark.parametrize("asynchronous", [False, True]) -async def test_event_sanitizer_flush_is_reentrant(capture_events, asynchronous): - _capture_name, events = capture_events - flush_returned = False - - def sanitize_sync(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: - nonlocal flush_returned - subscribers.flush() - flush_returned = True - return fields - - async def sanitize_async(_event: nemo_relay.Event, fields: EventSanitizeFields) -> EventSanitizeFields: - await asyncio.sleep(0) - return sanitize_sync(_event, fields) - - guardrails.register_mark_sanitize( - "python-reentrant-mark", - 0, - sanitize_async if asynchronous else sanitize_sync, - ) - try: - scope.event("reentrant-checkpoint", data={"raw": True}) - await subscribers.flush_async() - finally: - guardrails.deregister_mark_sanitize("python-reentrant-mark") - - assert flush_returned is True - assert events[-1].data == {"raw": True} - - def test_scope_start_and_end_sanitizers_cover_category_profile(capture_events): _capture_name, events = capture_events From 422f15e6453a49029b8e303719fe2681d5b8b954 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 12:51:53 -0400 Subject: [PATCH 58/83] fix: finalize async middleware contracts Signed-off-by: Will Killian --- crates/core/src/plugin/dynamic/native.rs | 81 ++-- .../tests/fixtures/native_plugin/src/lib.rs | 12 +- crates/core/tests/unit/native_plugin_tests.rs | 374 +++++++++++++++++- crates/ffi/README.md | 6 + crates/plugin/src/lib.rs | 13 +- docs/about-nemo-relay/concepts/middleware.mdx | 25 +- .../dynamic-plugins/native-dynamic/about.mdx | 6 + docs/reference/event-sanitizers.mdx | 7 +- docs/reference/migration-guides.mdx | 16 +- go/nemo_relay/README.md | 5 + 10 files changed, 490 insertions(+), 55 deletions(-) diff --git a/crates/core/src/plugin/dynamic/native.rs b/crates/core/src/plugin/dynamic/native.rs index 88d9d4650..35fe8eace 100644 --- a/crates/core/src/plugin/dynamic/native.rs +++ b/crates/core/src/plugin/dynamic/native.rs @@ -1334,7 +1334,7 @@ where struct NativeCallbackUserData { ptr: *mut c_void, free_fn: NemoRelayNativeFreeFn, - _instance: Arc, + _instance: Option>, } struct NativeCallbackUserDataGuard { @@ -1387,7 +1387,7 @@ fn make_user_data( Arc::new(NativeCallbackUserData { ptr: user_data, free_fn, - _instance: instance, + _instance: Some(instance), }) } @@ -1489,6 +1489,7 @@ struct NativeAsyncStreamCallbackGuard { cb: NemoRelayNativeAsyncNextStreamCb, user_data: usize, stream: Arc, + _library_guard: Option>, active: bool, } @@ -1498,22 +1499,39 @@ impl NativeAsyncStreamCallbackGuard { } fn fail(&mut self, error: &str) { - if self.active - && !self.stream.cancelled.load(Ordering::Acquire) - && let Some(message) = native_string_from_str(error) - { + if !self.active { + return; + } + // Cancellation owns terminal delivery. Leave the guard active so its + // Drop implementation can notify the plugin and release callback data. + if self.stream.cancelled.load(Ordering::Acquire) { + return; + } + if let Some(message) = native_string_from_str(error) { unsafe { let _ = (self.cb)(self.user_data as *mut c_void, ptr::null(), message, false); native_string_free(message); } + self.active = false; } - self.active = false; } } impl Drop for NativeAsyncStreamCallbackGuard { fn drop(&mut self) { - if self.active && !self.stream.cancelled.load(Ordering::Acquire) { + if !self.active { + return; + } + if self.stream.cancelled.load(Ordering::Acquire) { + if let Some(message) = + native_string_from_str("native async stream continuation was cancelled") + { + unsafe { + let _ = (self.cb)(self.user_data as *mut c_void, ptr::null(), message, false); + native_string_free(message); + } + } + } else { unsafe { let _ = (self.cb)( self.user_data as *mut c_void, @@ -1587,6 +1605,11 @@ async fn invoke_native_async_callback( before_settlement_lock: None, _callback_user_data: Some(user_data.clone()), }); + let mut wait = NativeAsyncWait { + completion: Arc::clone(&completion), + receiver, + completed: false, + }; let completion_ref = Arc::into_raw(completion.clone()) as usize; let next_ref = match (next, runtime) { (Some(inner), Some(runtime)) => Some(Arc::into_raw(Arc::new(NativeAsyncNext::new( @@ -1649,11 +1672,6 @@ async fn invoke_native_async_callback( )); } } - let mut wait = NativeAsyncWait { - completion, - receiver, - completed: false, - }; wait.receive().await } @@ -2056,9 +2074,15 @@ unsafe extern "C" fn native_async_next_invoke_stream( } let next_fn = next_fn.clone(); let continuation_context = next.context.clone(); - let library_guard = next._callback_user_data.clone(); let user_data = user_data as usize; let output_stream_for_task = Arc::clone(&output_stream); + let callback_guard = NativeAsyncStreamCallbackGuard { + cb, + user_data, + stream: output_stream_for_task, + _library_guard: next._callback_user_data.clone(), + active: true, + }; let (start_tx, start_rx) = tokio::sync::oneshot::channel(); let task = next.runtime.spawn(async move { if start_rx.await.is_err() { @@ -2066,13 +2090,7 @@ unsafe extern "C" fn native_async_next_invoke_stream( } continuation_context .run(async move { - let _library_guard = library_guard; - let mut callback_guard = NativeAsyncStreamCallbackGuard { - cb, - user_data, - stream: output_stream_for_task, - active: true, - }; + let mut callback_guard = callback_guard; let result = AssertUnwindSafe(async { match next_fn(request).await { Ok(mut stream) => { @@ -2419,6 +2437,13 @@ fn wrap_native_incremental_llm_stream_execution( free_fn: NemoRelayNativeFreeFn, ) -> LlmStreamExecutionFn { let user_data = make_user_data(instance, user_data, free_fn); + wrap_native_incremental_llm_stream_execution_with_user_data(cb, user_data) +} + +fn wrap_native_incremental_llm_stream_execution_with_user_data( + cb: NemoRelayNativeAsyncStreamMiddlewareCb, + user_data: Arc, +) -> LlmStreamExecutionFn { Arc::new(move |name, request, next| { let user_data = user_data.clone(); let name = name.to_owned(); @@ -2452,6 +2477,10 @@ fn wrap_native_incremental_llm_stream_execution( before_settlement_lock: None, _callback_user_data: Some(user_data.clone()), }); + let output = NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }; let stream_ref = Arc::into_raw(stream.clone()); let state = catch_unwind(AssertUnwindSafe(|| unsafe { cb( @@ -2468,11 +2497,6 @@ fn wrap_native_incremental_llm_stream_execution( { Some(state) => state, None => { - stream - .sender - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take(); return Err(FlowError::Internal( "native async stream callback panicked or returned an invalid state".into(), )); @@ -2489,10 +2513,7 @@ fn wrap_native_incremental_llm_stream_execution( "native async stream callback returned Complete without finishing".into(), )); } - Ok(LlmJsonStream::new(NativeAsyncStreamReceiver { - receiver, - stream, - })) + Ok(LlmJsonStream::new(output)) }) }) } diff --git a/crates/core/tests/fixtures/native_plugin/src/lib.rs b/crates/core/tests/fixtures/native_plugin/src/lib.rs index 6af594610..a8c47a1bc 100644 --- a/crates/core/tests/fixtures/native_plugin/src/lib.rs +++ b/crates/core/tests/fixtures/native_plugin/src/lib.rs @@ -741,8 +741,16 @@ unsafe extern "C" fn raw_async_stream_forward( ) -> bool { let state = unsafe { &*(user_data as *const AsyncStreamForward) }; if !chunk.is_null() { - return unsafe { (state.host.async_stream_push_json)(state.stream, chunk) } - == NemoRelayStatus::Ok; + if unsafe { (state.host.async_stream_push_json)(state.stream, chunk) } + == NemoRelayStatus::Ok + { + return true; + } + unsafe { + (state.host.async_stream_release)(state.stream); + drop(Box::from_raw(user_data as *mut AsyncStreamForward)); + } + return false; } if !error.is_null() { unsafe { (state.host.async_stream_reject)(state.stream, error) }; diff --git a/crates/core/tests/unit/native_plugin_tests.rs b/crates/core/tests/unit/native_plugin_tests.rs index 1e59cabc4..2e6e9a855 100644 --- a/crates/core/tests/unit/native_plugin_tests.rs +++ b/crates/core/tests/unit/native_plugin_tests.rs @@ -93,9 +93,22 @@ unsafe extern "C" fn accept_native_stream_item( struct NativeStreamCallbackState { error: Mutex>, done: AtomicBool, + callbacks: AtomicUsize, notified: tokio::sync::Notify, } +struct OwnedNativeStreamCallbackState { + result: Arc, + drop_count: Arc, + stream: *const NemoRelayNativeAsyncStream, +} + +impl Drop for OwnedNativeStreamCallbackState { + fn drop(&mut self) { + self.drop_count.fetch_add(1, Ordering::SeqCst); + } +} + unsafe extern "C" fn record_native_stream_result( user_data: *mut c_void, _chunk_json: *const NemoRelayNativeString, @@ -103,6 +116,7 @@ unsafe extern "C" fn record_native_stream_result( done: bool, ) -> bool { let state = unsafe { &*(user_data as *const NativeStreamCallbackState) }; + state.callbacks.fetch_add(1, Ordering::SeqCst); if !error.is_null() { *state .error @@ -114,6 +128,32 @@ unsafe extern "C" fn record_native_stream_result( true } +unsafe extern "C" fn record_and_release_native_stream_result( + user_data: *mut c_void, + chunk_json: *const NemoRelayNativeString, + error: *const NemoRelayNativeString, + done: bool, +) -> bool { + if !chunk_json.is_null() { + return true; + } + let state = unsafe { Box::from_raw(user_data as *mut OwnedNativeStreamCallbackState) }; + state.result.callbacks.fetch_add(1, Ordering::SeqCst); + if !error.is_null() { + *state + .result + .error + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = read_native_string(error).ok(); + } + state.result.done.store(done, Ordering::Release); + let result = Arc::clone(&state.result); + unsafe { native_async_stream_release(state.stream) }; + drop(state); + result.notified.notify_one(); + false +} + unsafe extern "C" fn stop_after_first_native_stream_item( user_data: *mut c_void, _chunk_json: *const NemoRelayNativeString, @@ -125,6 +165,78 @@ unsafe extern "C" fn stop_after_first_native_stream_item( false } +struct InvokeNativeNextThenReturnState { + callback_state: u32, + invoke_status: AtomicUsize, + started: Mutex>, +} + +unsafe extern "C" fn invoke_native_next_then_return_state( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + next: *const NemoRelayNativeAsyncNext, + completion: *const NemoRelayNativeAsyncCompletion, +) -> u32 { + let state = unsafe { &*user_data.cast::() }; + let status = unsafe { native_async_next_invoke(next, invocation_json, completion) }; + state + .invoke_status + .store(status as usize, Ordering::Release); + if status == NemoRelayStatus::Ok { + let _ = state + .started + .lock() + .unwrap_or_else(|error| error.into_inner()) + .recv_timeout(Duration::from_secs(1)); + } + unsafe { native_async_next_release(next) }; + state.callback_state +} + +unsafe extern "C" fn invoke_native_stream_next_then_return_state( + user_data: *mut c_void, + invocation_json: *const NemoRelayNativeString, + next: *const NemoRelayNativeAsyncNext, + stream: *const NemoRelayNativeAsyncStream, +) -> u32 { + let state = unsafe { &*user_data.cast::() }; + let request = read_native_string(invocation_json) + .ok() + .and_then(|invocation| serde_json::from_str::(&invocation).ok()) + .and_then(|invocation| invocation.get("request").cloned()) + .and_then(|request| native_string_from_json(&request)); + let status = if let Some(request) = request { + let status = unsafe { + native_async_next_invoke_stream( + next, + request, + stream, + accept_native_stream_item, + ptr::null_mut(), + ) + }; + unsafe { native_string_free(request) }; + status + } else { + NemoRelayStatus::InvalidJson + }; + state + .invoke_status + .store(status as usize, Ordering::Release); + if status == NemoRelayStatus::Ok { + let _ = state + .started + .lock() + .unwrap_or_else(|error| error.into_inner()) + .recv_timeout(Duration::from_secs(1)); + } + unsafe { + native_async_next_release(next); + native_async_stream_release(stream); + } + state.callback_state +} + struct FailingNativeCodec; impl LlmCodec for FailingNativeCodec { @@ -1144,7 +1256,7 @@ fn native_async_stream_next_stops_callbacks_after_false() { } #[test] -fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { +fn native_async_stream_in_flight_cancellation_releases_callback_state() { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -1184,7 +1296,13 @@ fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { .unwrap(), ) .unwrap(); - let callbacks = AtomicUsize::new(0); + let callback_state = Arc::new(NativeStreamCallbackState::default()); + let drop_count = Arc::new(AtomicUsize::new(0)); + let callback_user_data = Box::into_raw(Box::new(OwnedNativeStreamCallbackState { + result: Arc::clone(&callback_state), + drop_count: Arc::clone(&drop_count), + stream: stream_ref, + })); assert_eq!( unsafe { @@ -1192,8 +1310,8 @@ fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { next_ref, invocation, stream_ref, - stop_after_first_native_stream_item, - (&callbacks as *const AtomicUsize).cast_mut().cast(), + record_and_release_native_stream_result, + callback_user_data.cast(), ) }, NemoRelayStatus::Ok @@ -1203,13 +1321,110 @@ fn native_async_stream_consumer_cancellation_suppresses_terminal_callback() { receiver, stream: Arc::clone(&stream), }); + runtime + .block_on(async { + tokio::time::timeout(Duration::from_secs(1), callback_state.notified.notified()).await + }) + .expect("in-flight cancellation should deliver a terminal callback"); runtime.block_on(tokio::task::yield_now()); - assert_eq!(callbacks.load(Ordering::SeqCst), 0); + assert_eq!(callback_state.callbacks.load(Ordering::SeqCst), 1); + assert_eq!(drop_count.load(Ordering::SeqCst), 1); + assert!(!callback_state.done.load(Ordering::Acquire)); + assert!( + callback_state + .error + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .as_deref() + .is_some_and(|error| error.contains("cancelled")) + ); + assert_eq!(Arc::strong_count(&stream), 1); + + unsafe { + native_string_free(invocation); + native_async_next_release(next_ref); + } +} + +#[test] +fn native_async_stream_cancellation_before_first_poll_releases_callback_state() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let next = Arc::new(NativeAsyncNext::new( + NativeAsyncNextInner::LlmStream(Arc::new(move |_request| Box::pin(std::future::pending()))), + runtime.handle().clone(), + None, + )); + let next_ref = Arc::into_raw(next) as *const NemoRelayNativeAsyncNext; + let (sender, receiver) = tokio::sync::mpsc::channel(1); + let stream = Arc::new(NativeAsyncStream { + sender: Mutex::new(Some(sender)), + cancelled: AtomicBool::new(false), + next_invoked: AtomicBool::new(false), + downstream_abort: Mutex::new(None), + settlement: Mutex::new(()), + before_settlement_lock: None, + _callback_user_data: None, + }); + let stream_ref = Arc::into_raw(Arc::clone(&stream)) as *const NemoRelayNativeAsyncStream; + let invocation = native_string_from_json( + &serde_json::to_value(LlmRequest { + headers: Map::new(), + content: json!({"stream": true}), + }) + .unwrap(), + ) + .unwrap(); + let callback_state = Arc::new(NativeStreamCallbackState::default()); + let drop_count = Arc::new(AtomicUsize::new(0)); + let callback_user_data = Box::into_raw(Box::new(OwnedNativeStreamCallbackState { + result: Arc::clone(&callback_state), + drop_count: Arc::clone(&drop_count), + stream: stream_ref, + })); + + assert_eq!( + unsafe { + native_async_next_invoke_stream( + next_ref, + invocation, + stream_ref, + record_and_release_native_stream_result, + callback_user_data.cast(), + ) + }, + NemoRelayStatus::Ok + ); + // The current-thread runtime has not been driven, so cancellation happens + // before the spawned continuation can be polled for the first time. + drop(NativeAsyncStreamReceiver { + receiver, + stream: Arc::clone(&stream), + }); + runtime + .block_on(async { + tokio::time::timeout(Duration::from_secs(1), callback_state.notified.notified()).await + }) + .expect("pre-poll cancellation should deliver a terminal callback"); + runtime.block_on(tokio::task::yield_now()); + assert_eq!(callback_state.callbacks.load(Ordering::SeqCst), 1); + assert_eq!(drop_count.load(Ordering::SeqCst), 1); + assert!(!callback_state.done.load(Ordering::Acquire)); + assert!( + callback_state + .error + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .as_deref() + .is_some_and(|error| error.contains("cancelled")) + ); + assert_eq!(Arc::strong_count(&stream), 1); unsafe { native_string_free(invocation); native_async_next_release(next_ref); - native_async_stream_release(stream_ref); } } @@ -1464,6 +1679,153 @@ fn cancelling_completion_aborts_pending_native_next() { } } +#[test] +fn native_async_callback_contract_errors_abort_an_invoked_next() { + struct DropSignal(std::sync::mpsc::Sender<()>); + + impl Drop for DropSignal { + fn drop(&mut self) { + let _ = self.0.send(()); + } + } + + for (callback_state, expected_error) in [ + ( + NemoRelayNativeAsyncCallbackState::Complete as u32, + "returned Complete without settling", + ), + (99, "returned an invalid state"), + ] { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .unwrap(); + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (dropped_tx, dropped_rx) = std::sync::mpsc::channel(); + let state = InvokeNativeNextThenReturnState { + callback_state, + invoke_status: AtomicUsize::new(NemoRelayStatus::Internal as usize), + started: Mutex::new(started_rx), + }; + let user_data = Arc::new(NativeCallbackUserData { + ptr: (&state as *const InvokeNativeNextThenReturnState) + .cast_mut() + .cast(), + free_fn: None, + _instance: None, + }); + + let error = runtime + .block_on(invoke_native_async_callback( + invoke_native_next_then_return_state, + user_data, + json!({}), + Some(NativeAsyncNextInner::Tool(Arc::new({ + let started_tx = started_tx.clone(); + let dropped_tx = dropped_tx.clone(); + move |_value| { + let started_tx = started_tx.clone(); + let guard = DropSignal(dropped_tx.clone()); + Box::pin(async move { + let _guard = guard; + let _ = started_tx.send(()); + std::future::pending::>().await + }) + } + }))), + )) + .unwrap_err(); + + assert!(error.to_string().contains(expected_error)); + assert_eq!( + state.invoke_status.load(Ordering::Acquire), + NemoRelayStatus::Ok as usize + ); + dropped_rx + .recv_timeout(Duration::from_secs(1)) + .expect("contract error should abort and drop the pending native next"); + } +} + +#[test] +fn native_async_stream_contract_errors_abort_an_invoked_next() { + struct DropSignal(std::sync::mpsc::Sender<()>); + + impl Drop for DropSignal { + fn drop(&mut self) { + let _ = self.0.send(()); + } + } + + for (callback_state, expected_error) in [ + ( + NemoRelayNativeAsyncCallbackState::Complete as u32, + "returned Complete without finishing", + ), + (99, "panicked or returned an invalid state"), + ] { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .unwrap(); + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (dropped_tx, dropped_rx) = std::sync::mpsc::channel(); + let state = InvokeNativeNextThenReturnState { + callback_state, + invoke_status: AtomicUsize::new(NemoRelayStatus::Internal as usize), + started: Mutex::new(started_rx), + }; + let user_data = Arc::new(NativeCallbackUserData { + ptr: (&state as *const InvokeNativeNextThenReturnState) + .cast_mut() + .cast(), + free_fn: None, + _instance: None, + }); + let wrapped = wrap_native_incremental_llm_stream_execution_with_user_data( + invoke_native_stream_next_then_return_state, + user_data, + ); + let next: LlmStreamExecutionNextFn = Arc::new({ + let started_tx = started_tx.clone(); + let dropped_tx = dropped_tx.clone(); + move |_request| { + let started_tx = started_tx.clone(); + let guard = DropSignal(dropped_tx.clone()); + Box::pin(async move { + let _guard = guard; + let _ = started_tx.send(()); + std::future::pending::>().await + }) + } + }); + + let result = runtime.block_on(wrapped( + "contract-error", + LlmRequest { + headers: Map::new(), + content: Json::Null, + }, + next, + )); + let error = match result { + Err(error) => error, + Ok(_) => panic!("contract error unexpectedly returned a stream"), + }; + + assert!(error.to_string().contains(expected_error)); + assert_eq!( + state.invoke_status.load(Ordering::Acquire), + NemoRelayStatus::Ok as usize + ); + dropped_rx + .recv_timeout(Duration::from_secs(1)) + .expect("contract error should abort and drop the pending native stream next"); + } +} + #[test] fn native_async_stream_settlement_cannot_succeed_after_cancellation() { #[derive(Clone, Copy)] diff --git a/crates/ffi/README.md b/crates/ffi/README.md index f54af7c61..c54453cb0 100644 --- a/crates/ffi/README.md +++ b/crates/ffi/README.md @@ -57,6 +57,12 @@ binding consumes it through CGo. - **Go binding foundation**: The repository-maintained Go binding consumes this ABI through CGo. +Middleware callbacks in the raw C ABI are synchronous. Relay invokes a +callback on a native thread and waits for it to return. Blocking I/O and other +long-running callback work therefore occupy that thread and can reduce +middleware throughput. The FFI does not expose completion-based middleware +registration. + ## Installation Build the FFI library from a repository checkout: diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 94288af05..7cc57900e 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -872,8 +872,12 @@ pub struct NemoRelayNativeAsyncStream { } /// Receives one downstream stream item. `chunk_json` is non-null for a chunk, -/// `error` is non-null for failure, and `done` marks clean completion. Return -/// `false` to cancel downstream production after the current callback. +/// `error` is non-null for failure or consumer cancellation, and `done` marks +/// clean completion. Unless the callback itself returns `false`, the host +/// invokes one terminal callback so the plugin can reclaim `user_data`. +/// Return `false` to cancel downstream production after the current callback; +/// in that case, reclaim `user_data` before returning because no later callback +/// is made. pub type NemoRelayNativeAsyncNextStreamCb = unsafe extern "C" fn( user_data: *mut c_void, chunk_json: *const NemoRelayNativeString, @@ -1002,6 +1006,11 @@ pub struct NemoRelayNativeHostApiV3 { /// Releases the callback-owned incremental stream reference. pub async_stream_release: unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream), /// Invokes a downstream stream and reports chunks incrementally. + /// + /// The host reports consumer cancellation through one terminal callback + /// with a non-null error. If a result callback returns `false`, it must + /// reclaim its own `user_data` before returning because no terminal + /// callback follows. pub async_next_invoke_stream: unsafe extern "C" fn( next: *const NemoRelayNativeAsyncNext, invocation_json: *const NemoRelayNativeString, diff --git a/docs/about-nemo-relay/concepts/middleware.mdx b/docs/about-nemo-relay/concepts/middleware.mdx index 55bdad16f..e157792f7 100644 --- a/docs/about-nemo-relay/concepts/middleware.mdx +++ b/docs/about-nemo-relay/concepts/middleware.mdx @@ -21,14 +21,23 @@ hook system. ## Asynchronous Callbacks -All middleware families accept asynchronous callbacks. Rust callbacks return a -future, and Node callbacks may return a value or a Promise. Python registrations -accept callbacks that return a value or an awaitable when invoked through an -asynchronous Relay API or queued event publication. Synchronous standalone -Python calls cannot drive an awaitable callback. Call the same standalone -helper from a running event loop and await the returned value instead. Relay -awaits entries sequentially in priority order, so later callbacks observe -earlier middleware output. +All middleware families are asynchronous in the Rust runtime. Rust callbacks +return a future, and Node callbacks may return a value or a Promise. Python +registrations accept callbacks that return a value or an awaitable when invoked +through an asynchronous Relay API or queued event publication. Worker and +native-plugin middleware can also complete asynchronously. Relay awaits entries +sequentially in priority order, so later callbacks observe earlier middleware +output. + +The experimental raw C FFI and Go binding retain synchronous middleware +callbacks. Relay invokes each callback on a native thread and waits for it to +return, so blocking I/O or other long-running work occupies that thread and can +reduce middleware throughput. There is no completion-based C or Go middleware +registration API. + +Synchronous standalone Python calls cannot drive an awaitable callback. Call +the same standalone helper from a running event loop and await the returned +value instead. Managed execution is asynchronous because its result depends on middleware completion. Python standalone conditional and request-intercept helpers return diff --git a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx index f3a7d19e8..253fbfbc7 100644 --- a/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx +++ b/docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx @@ -175,6 +175,12 @@ chunk or rejection and retry after the consumer advances. `InvalidArg` means the stream is already closed or cancelled and must not be retried. Check `async_stream_is_cancelled` during longer producer work. +For `async_next_invoke_stream`, Relay reports downstream failure or consumer +cancellation through one terminal callback with a non-null error, and reports +clean completion with `done = true`. Reclaim the callback's `user_data` in that +terminal callback. If a chunk callback returns `false`, reclaim `user_data` +before returning because Relay does not invoke another callback afterward. + Cancelling the one-shot completion supplied to a non-stream execution intercept also aborts any pending `async_next_invoke` continuation. Plugins must still release their callback-owned completion and `next` references diff --git a/docs/reference/event-sanitizers.mdx b/docs/reference/event-sanitizers.mdx index 6a139aa07..5d27bf4bc 100644 --- a/docs/reference/event-sanitizers.mdx +++ b/docs/reference/event-sanitizers.mdx @@ -209,14 +209,17 @@ activation fails. The source-first C API uses `NemoRelayEventSanitizeCb`. It provides global, scope-local, and plugin-context registration functions for all three surfaces. Global names start with `nemo_relay_register_`, and scope-local names start -with `nemo_relay_scope_register_`. +with `nemo_relay_scope_register_`. These callbacks are synchronous: Relay +waits on a native thread until the callback returns, and no completion-based +registration variant is provided. The Go binding provides `EventSanitizeFields`, `EventSanitizeFunc`, global `Register*SanitizeGuardrail` helpers, scope-local `ScopeRegister*SanitizeGuardrail` helpers, and the same methods on `PluginContext`. The `guardrails` package provides shorter aliases. Because a returned `EventSanitizeFields` replaces all three fields, copy the supplied -value and modify only the fields that should change. +value and modify only the fields that should change. Go middleware callbacks +are also synchronous; blocking work occupies the native callback thread. ## Related Topics diff --git a/docs/reference/migration-guides.mdx b/docs/reference/migration-guides.mdx index 48e3aad38..8e11c37ea 100644 --- a/docs/reference/migration-guides.mdx +++ b/docs/reference/migration-guides.mdx @@ -42,6 +42,11 @@ existing error behavior for its middleware family. | Node.js | Direct return value | Direct return value or `Promise` | | Go / raw C FFI | Synchronous callback | Synchronous callback | +The experimental Go and raw C FFI callbacks remain synchronous. Relay waits +for each callback on a native thread, so blocking I/O and other long-running +callback work occupy that thread. No completion-based registration API is +provided for these bindings. + Python's standalone middleware helpers preserve their direct synchronous return when called without a running `asyncio` loop. In that mode, registered callbacks must also return direct values; an awaitable callback raises a clear @@ -316,7 +321,7 @@ The codec capability ID in the protocol is SDK-internal. Do not expose or persist it in plugin code. The host rejects forged, expired, unauthorized, and wrong-direction capability IDs. -### Rebuild Native and Raw FFI Plugins +### Rebuild Native Plugins and Raw FFI Consumers NeMo Relay 0.7 uses native ABI v3. Recompile native plugins against the 0.7 `nemo-relay-plugin` crate and rebuild raw FFI consumers against the generated @@ -325,9 +330,10 @@ NeMo Relay 0.7 uses native ABI v3. Recompile native plugins against the 0.7 The v3 table preserves the v2 prefix, and Relay retries a legacy v2 table when loading a plugin that rejects v3. That fallback supports loading, not compatibility with changed middleware, LLM sanitizer, ABI, or schema contracts. -Rebuild plugins that use raw ABI callbacks: v3 adds completion-based async -middleware registration, async execution continuations, and explicit -cancellation/late-settlement behavior. +Rebuild native plugins that use the raw plugin ABI callbacks: native ABI v3 +adds completion-based async middleware registration, async execution +continuations, and explicit cancellation/late-settlement behavior. This is +separate from the synchronous `nemo-relay-ffi` middleware registration API. The plugin manifest value remains `compat.native_api = "1"`. This manifest contract version is separate from the host ABI version; do not change it to @@ -395,7 +401,7 @@ Before deployment: 2. Add the required directional context argument and optional payload result. 3. Check that no callback implicitly returns `None`, `null`, or `undefined`. 4. Convert every Rust worker sanitizer callback to an asynchronous callback. -5. Rebuild native and raw FFI plugins against 0.7. +5. Rebuild native plugins and raw FFI consumers against 0.7. 6. Regenerate and redeploy workers with the matching 0.7 protocol and SDK. 7. Remove fixed PII `codec` values from mixed-provider configurations. 8. Test buffered and streaming calls for every provider and custom codec that diff --git a/go/nemo_relay/README.md b/go/nemo_relay/README.md index 7df389c52..57bbca94c 100644 --- a/go/nemo_relay/README.md +++ b/go/nemo_relay/README.md @@ -61,6 +61,11 @@ The Go package provides the following capabilities: - **Local source-first workflow**: Build the FFI library locally, then test or consume the Go module from the checkout. +Go middleware callbacks are synchronous. Relay waits for each callback on a +native thread, so blocking I/O and other long-running callback work occupy that +thread and can reduce middleware throughput. The Go binding does not provide +completion-based middleware registration. + ## Installation Build the FFI library from a repository checkout before using the Go binding: From b320a03daa51ce26ff0a62d86176faf463270540 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 12:57:34 -0400 Subject: [PATCH 59/83] chore: refresh stacked PR checks Signed-off-by: Will Killian From 8763e5d74628ff86841f93286170b7f13147702f Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 14:34:32 -0400 Subject: [PATCH 60/83] chore: refresh Rust attributions Signed-off-by: Will Killian --- ATTRIBUTIONS-Rust.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index a9df3ef75..ee60e8a33 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -26115,8 +26115,9 @@ SOFTWARE. ## md-5 - 0.11.0 **Repository URL**: https://github.com/RustCrypto/hashes -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +**License Type(s)**: MIT OR Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -26321,6 +26322,38 @@ See the License for the specific language governing permissions and limitations under the License. ``` +### License File: LICENSE-MIT +``` +Copyright (c) 2016-2026 The RustCrypto Project Developers +Copyright (c) 2016 Artyom Pavlov +Copyright (c) 2009-2013 Mozilla Foundation +Copyright (c) 2006-2009 Graydon Hoare + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` + ## memchr - 2.8.0 **Repository URL**: https://github.com/BurntSushi/memchr **License Type(s)**: MIT From 2e187fed2c4be101f339d836469be327eeb3081c Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 15:23:47 -0400 Subject: [PATCH 61/83] test(plugin): flush subscriber-emitted events Signed-off-by: Will Killian --- crates/core/tests/integration/native_plugin_tests.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/core/tests/integration/native_plugin_tests.rs b/crates/core/tests/integration/native_plugin_tests.rs index 82fc2168e..28380fdb3 100644 --- a/crates/core/tests/integration/native_plugin_tests.rs +++ b/crates/core/tests/integration/native_plugin_tests.rs @@ -268,6 +268,10 @@ async fn sdk_cdylib_registers_tool_request_intercept() { assert!(tool_result.get("pending_marks").is_none()); flush_subscribers().expect("native fixture events should flush"); + // The native subscriber emits its mark while handling the outer event. + // A second barrier drains publications queued by callbacks before the + // first barrier completed. + flush_subscribers().expect("native fixture subscriber events should flush"); let first_events = events.lock().unwrap().clone(); find_event(&first_events, "fixture.native.subscriber.mark", None); assert_parent(&first_events, "fixture.native.mark", None, Some(outer_uuid)); From 9996065bb8a07b3d70b98d9f08f5b076cedb28ab Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 16:07:40 -0400 Subject: [PATCH 62/83] fix: flush transitive subscriber publications Signed-off-by: Will Killian --- .../src/api/runtime/subscriber_dispatcher.rs | 232 +++++++++++++++--- crates/core/src/api/subscriber.rs | 3 +- .../tests/integration/native_plugin_tests.rs | 4 - .../tests/unit/subscriber_dispatcher_tests.rs | 179 +++++++++++++- crates/ffi/nemo_relay.h | 3 +- crates/ffi/src/api/llm_registry.rs | 3 +- crates/node/README.md | 8 +- crates/node/src/api/mod.rs | 2 + crates/python/src/py_api/mod.rs | 2 +- go/nemo_relay/nemo_relay.go | 12 +- python/nemo_relay/_native.pyi | 2 +- python/nemo_relay/subscribers.py | 4 +- 12 files changed, 394 insertions(+), 60 deletions(-) diff --git a/crates/core/src/api/runtime/subscriber_dispatcher.rs b/crates/core/src/api/runtime/subscriber_dispatcher.rs index bbdfea04c..9af0fbf3f 100644 --- a/crates/core/src/api/runtime/subscriber_dispatcher.rs +++ b/crates/core/src/api/runtime/subscriber_dispatcher.rs @@ -86,8 +86,9 @@ mod native { use std::cell::{Cell, RefCell}; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Mutex; - use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; + use std::sync::{Arc, Weak}; use super::*; #[cfg(test)] @@ -106,15 +107,41 @@ mod native { subscribers: Vec, scope_stack: ScopeStackHandle, publication_context: Option, + lineage: Option, }, Flush { done: Sender<()>, }, Barrier { publications: Receiver>, + lineage: Option, }, } + #[derive(Default)] + pub(super) struct PublicationLineage { + outstanding: AtomicUsize, + } + + pub(super) struct PublicationPermit(Arc); + + impl PublicationPermit { + pub(super) fn new(lineage: Arc) -> Self { + lineage.outstanding.fetch_add(1, Ordering::AcqRel); + Self(lineage) + } + + fn lineage(&self) -> Arc { + Arc::clone(&self.0) + } + } + + impl Drop for PublicationPermit { + fn drop(&mut self) { + self.0.outstanding.fetch_sub(1, Ordering::AcqRel); + } + } + type DispatcherState = Option, String>>; type SanitizerRuntimeState = Option>; type BackgroundPublication = Pin + Send + 'static>>; @@ -126,12 +153,14 @@ mod native { #[derive(Clone)] pub struct PublicationBuffer { messages: Arc>>>, + lineage: Option>, } impl PublicationBuffer { fn new(messages: Option>) -> Self { Self { messages: Arc::new(Mutex::new(messages)), + lineage: current_publication_lineage(), } } @@ -199,6 +228,7 @@ mod native { static IN_DISPATCHER: Cell = const { Cell::new(false) }; static PREPARED_FORK_STATE: Cell<*mut ProcessState> = const { Cell::new(std::ptr::null_mut()) }; static THREAD_PUBLICATION_BUFFER: RefCell> = const { RefCell::new(None) }; + static THREAD_PUBLICATION_LINEAGE: RefCell>> = const { RefCell::new(None) }; } tokio::task_local! { static ASYNC_PUBLICATION_BUFFER: PublicationBuffer; @@ -206,6 +236,7 @@ mod native { struct DispatchGuard; struct ThreadPublicationBufferGuard(Option); + struct ThreadPublicationLineageGuard(Option>); pub(crate) struct AsyncPublication { pub(super) sender: Sender>, @@ -252,6 +283,58 @@ mod native { } } + impl Drop for ThreadPublicationLineageGuard { + fn drop(&mut self) { + THREAD_PUBLICATION_LINEAGE.with(|current| { + current.replace(self.0.take()); + }); + } + } + + fn current_publication_lineage() -> Option> { + ASYNC_PUBLICATION_BUFFER + .try_with(|buffer| buffer.lineage.clone()) + .ok() + .flatten() + .or_else(|| { + THREAD_PUBLICATION_BUFFER.with(|buffer| { + buffer + .borrow() + .as_ref() + .and_then(|buffer| buffer.lineage.clone()) + }) + }) + .or_else(|| THREAD_PUBLICATION_LINEAGE.with(|lineage| lineage.borrow().clone())) + } + + fn with_publication_lineage(lineage: Arc, f: impl FnOnce() -> T) -> T { + let previous = THREAD_PUBLICATION_LINEAGE.with(|current| current.replace(Some(lineage))); + let _guard = ThreadPublicationLineageGuard(previous); + f() + } + + fn attach_publication_lineage( + message: &mut DispatcherMessage, + inherited: Option<&Arc>, + ) -> Arc { + let current = inherited + .cloned() + .or_else(current_publication_lineage) + .unwrap_or_default(); + match message { + DispatcherMessage::Deliver { lineage, .. } + | DispatcherMessage::Barrier { lineage, .. } => { + if let Some(permit) = lineage { + permit.lineage() + } else { + *lineage = Some(PublicationPermit::new(Arc::clone(¤t))); + current + } + } + DispatcherMessage::Flush { .. } => current, + } + } + fn immutable_scope_stack(scope_stack: &ScopeStackHandle) -> Option { match snapshot_scope_stack(scope_stack) { Ok(scope_stack) => Some(scope_stack), @@ -363,6 +446,7 @@ mod native { subscribers: subscribers.to_vec(), scope_stack, publication_context: current_publication_context(), + lineage: None, }; send_dispatch_message(message) } @@ -386,6 +470,7 @@ mod native { subscribers: subscribers.to_vec(), scope_stack, publication_context: current_publication_context(), + lineage: None, }; enqueue_dispatch_message(message) } @@ -409,6 +494,7 @@ mod native { subscribers: subscribers.to_vec(), scope_stack, publication_context: current_publication_context(), + lineage: None, }; enqueue_dispatch_message(message) } @@ -430,6 +516,7 @@ mod native { subscribers: subscribers.to_vec(), scope_stack, publication_context: current_publication_context(), + lineage: None, }; enqueue_dispatch_message(message) } @@ -441,6 +528,7 @@ mod native { let (publication_tx, publication_rx) = mpsc::channel(); enqueue_dispatch_message(DispatcherMessage::Barrier { publications: publication_rx, + lineage: None, }) .then_some(AsyncPublication { sender: publication_tx, @@ -548,7 +636,8 @@ mod native { dispatcher.get_or_insert_with(start_dispatcher).clone() } - fn send_dispatch_message(message: DispatcherMessage) -> bool { + fn send_dispatch_message(mut message: DispatcherMessage) -> bool { + attach_publication_lineage(&mut message, None); match dispatcher_sender() { Ok(sender) if sender.send(message).is_ok() => true, Ok(_) => { @@ -576,7 +665,8 @@ mod native { } } - pub(super) fn enqueue_dispatch_message(message: DispatcherMessage) -> bool { + pub(super) fn enqueue_dispatch_message(mut message: DispatcherMessage) -> bool { + attach_publication_lineage(&mut message, None); let message = if let Ok(buffer) = ASYNC_PUBLICATION_BUFFER.try_with(Clone::clone) { match buffer.push(message) { Ok(()) => return true, @@ -615,25 +705,85 @@ mod native { sender } + pub(super) struct PendingFlush { + pub(super) done: Sender<()>, + pub(super) lineages: Vec>, + } + + #[derive(Default)] + pub(super) struct DispatcherLoopState { + pub(super) active_lineages: Vec>, + pub(super) pending_flushes: Vec, + } + + impl DispatcherLoopState { + fn register(&mut self, lineage: &Arc) { + if !self.active_lineages.iter().any(|active| { + active + .upgrade() + .is_some_and(|active| Arc::ptr_eq(&active, lineage)) + }) { + self.active_lineages.push(Arc::downgrade(lineage)); + } + } + + fn defer_or_complete_flush(&mut self, done: Sender<()>) { + let lineages = self + .active_lineages + .iter() + .filter_map(Weak::upgrade) + .filter(|lineage| lineage.outstanding.load(Ordering::Acquire) > 0) + .collect::>(); + if lineages.is_empty() { + let _ = done.send(()); + } else { + self.pending_flushes.push(PendingFlush { done, lineages }); + } + } + + pub(super) fn complete_ready_flushes(&mut self) { + self.active_lineages.retain(|lineage| { + lineage + .upgrade() + .is_some_and(|lineage| lineage.outstanding.load(Ordering::Acquire) > 0) + }); + let ready = self + .pending_flushes + .iter() + .take_while(|flush| { + flush + .lineages + .iter() + .all(|lineage| lineage.outstanding.load(Ordering::Acquire) == 0) + }) + .count(); + for flush in self.pending_flushes.drain(..ready) { + let _ = flush.done.send(()); + } + } + } + fn run_dispatcher(rx: Receiver) { + let mut state = DispatcherLoopState::default(); while let Ok(message) = rx.recv() { - match message { - DispatcherMessage::Flush { done } => { - let _ = done.send(()); - } - DispatcherMessage::Barrier { publications } => { - if let Ok(publications) = publications.recv() { - for publication in publications { - handle_message(publication); - } - } - } - message => handle_message(message), - } + handle_message(message, &mut state, None); + state.complete_ready_flushes(); } } - fn handle_message(message: DispatcherMessage) { + fn handle_message( + mut message: DispatcherMessage, + state: &mut DispatcherLoopState, + inherited: Option<&Arc>, + ) { + let lineage = match message { + DispatcherMessage::Flush { done } => { + state.defer_or_complete_flush(done); + return; + } + _ => attach_publication_lineage(&mut message, inherited), + }; + state.register(&lineage); match message { DispatcherMessage::Deliver { event, @@ -642,25 +792,37 @@ mod native { subscribers, scope_stack, publication_context, - } => deliver_event( - event, - transform, - sanitizers, - subscribers, - scope_stack, - publication_context, - ), - DispatcherMessage::Flush { done } => { - let _ = done.send(()); + lineage: permit, + } => { + let nested_publications = with_publication_lineage(Arc::clone(&lineage), || { + deliver_event( + event, + transform, + sanitizers, + subscribers, + scope_stack, + publication_context, + ) + }); + drop(permit); + for publication in nested_publications { + handle_message(publication, state, Some(&lineage)); + } } - DispatcherMessage::Barrier { publications } => { + DispatcherMessage::Barrier { + publications, + lineage: permit, + } => { if let Ok(publications) = publications.recv() { for publication in publications { - handle_message(publication); + handle_message(publication, state, Some(&lineage)); } } + drop(permit); } + DispatcherMessage::Flush { .. } => unreachable!(), } + state.complete_ready_flushes(); } fn deliver_event( @@ -670,7 +832,7 @@ mod native { subscribers: Vec, scope_stack: ScopeStackHandle, publication_context: Option, - ) { + ) -> Vec { let previous_scope_stack = capture_thread_scope_stack(); set_thread_scope_stack(scope_stack); let _dispatch_guard = DispatchGuard::enter(); @@ -688,12 +850,7 @@ mod native { } } restore_thread_scope_stack(previous_scope_stack); - // Publications emitted while transforming or sanitizing this event - // are causally nested within it. Drain them before the dispatcher - // consumes messages that callers may already have queued afterward. - for publication in nested_publications { - handle_message(publication); - } + nested_publications } fn run_with_nested_publication_buffer( @@ -964,7 +1121,8 @@ where native::spawn_background_publication(future) } -/// Wait for all queued subscriber callbacks submitted before this call. +/// Wait for all queued subscriber callbacks submitted before this call, +/// including publications emitted transitively by those callbacks. pub fn flush_subscribers() -> Result<()> { native::flush_subscribers() } diff --git a/crates/core/src/api/subscriber.rs b/crates/core/src/api/subscriber.rs index 337544167..ba7052c8e 100644 --- a/crates/core/src/api/subscriber.rs +++ b/crates/core/src/api/subscriber.rs @@ -70,7 +70,8 @@ pub fn deregister_subscriber(name: &str) -> Result { Ok(state.event_subscribers.remove(name).is_some()) } -/// Wait for all subscriber callbacks queued before this call to finish. +/// Wait for all subscriber callbacks queued before this call to finish, +/// including publications emitted transitively by those callbacks. /// /// A direct re-entrant call from queued publication middleware returns without /// waiting. Publication middleware must not move such a flush into diff --git a/crates/core/tests/integration/native_plugin_tests.rs b/crates/core/tests/integration/native_plugin_tests.rs index 28380fdb3..82fc2168e 100644 --- a/crates/core/tests/integration/native_plugin_tests.rs +++ b/crates/core/tests/integration/native_plugin_tests.rs @@ -268,10 +268,6 @@ async fn sdk_cdylib_registers_tool_request_intercept() { assert!(tool_result.get("pending_marks").is_none()); flush_subscribers().expect("native fixture events should flush"); - // The native subscriber emits its mark while handling the outer event. - // A second barrier drains publications queued by callbacks before the - // first barrier completed. - flush_subscribers().expect("native fixture subscriber events should flush"); let first_events = events.lock().unwrap().clone(); find_event(&first_events, "fixture.native.subscriber.mark", None); assert_parent(&first_events, "fixture.native.mark", None, Some(outer_uuid)); diff --git a/crates/core/tests/unit/subscriber_dispatcher_tests.rs b/crates/core/tests/unit/subscriber_dispatcher_tests.rs index bd56e7a0a..082e29db1 100644 --- a/crates/core/tests/unit/subscriber_dispatcher_tests.rs +++ b/crates/core/tests/unit/subscriber_dispatcher_tests.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use super::EventSubscriberFn; use super::native::{ - DispatcherMessage, dispatcher_sender, enqueue_dispatch_message, flush_subscribers, - register_async_publication, sanitize_event_snapshot, set_sanitizer_runtime_failure_for_test, - spawn_background_publication, + DispatcherLoopState, DispatcherMessage, PendingFlush, PublicationLineage, PublicationPermit, + dispatcher_sender, enqueue_dispatch_message, flush_subscribers, register_async_publication, + sanitize_event_snapshot, set_sanitizer_runtime_failure_for_test, spawn_background_publication, }; use crate::api::registry::RegistryRecord; use crate::api::runtime::EventSanitizeFn; @@ -45,6 +45,7 @@ fn flush_waits_for_active_but_not_later_publication_barriers() { subscribers: vec![subscriber.clone()], scope_stack: current_scope_stack(), publication_context: None, + lineage: None, }) .unwrap(); let (flush_tx, flush_rx) = mpsc::channel(); @@ -76,6 +77,7 @@ fn flush_waits_for_active_but_not_later_publication_barriers() { subscribers: vec![subscriber], scope_stack: current_scope_stack(), publication_context: None, + lineage: None, }]) .unwrap(); flush_rx @@ -125,6 +127,7 @@ fn flush_does_not_wait_for_later_delivery() { subscribers: Vec::new(), scope_stack: current_scope_stack(), publication_context: None, + lineage: None, }) .unwrap(); barrier.sender.send(Vec::new()).unwrap(); @@ -138,6 +141,47 @@ fn flush_does_not_wait_for_later_delivery() { ); } +#[test] +fn pending_flushes_do_not_acknowledge_out_of_order() { + let first_lineage = Arc::new(PublicationLineage::default()); + let second_lineage = Arc::new(PublicationLineage::default()); + let first_permit = PublicationPermit::new(Arc::clone(&first_lineage)); + let second_permit = PublicationPermit::new(Arc::clone(&second_lineage)); + let (first_tx, first_rx) = mpsc::channel(); + let (second_tx, second_rx) = mpsc::channel(); + let mut state = DispatcherLoopState { + active_lineages: Vec::new(), + pending_flushes: vec![ + PendingFlush { + done: first_tx, + lineages: vec![first_lineage], + }, + PendingFlush { + done: second_tx, + lineages: vec![second_lineage], + }, + ], + }; + + drop(second_permit); + state.complete_ready_flushes(); + assert!(matches!( + first_rx.try_recv(), + Err(mpsc::TryRecvError::Empty) + )); + assert!( + matches!(second_rx.try_recv(), Err(mpsc::TryRecvError::Empty)), + "a ready later flush must not overtake an earlier pending flush" + ); + + drop(first_permit); + state.complete_ready_flushes(); + first_rx.recv().expect("first flush should complete first"); + second_rx + .recv() + .expect("second flush should complete afterward"); +} + #[test] fn nested_publication_barrier_precedes_already_queued_delivery() { let _lock = crate::shared_runtime::runtime_owner_test_mutex() @@ -192,6 +236,7 @@ fn nested_publication_barrier_precedes_already_queued_delivery() { subscribers: vec![nested_subscriber.clone()], scope_stack: nested_scope_stack.clone(), publication_context: None, + lineage: None, })); let publication = register_async_publication().expect("nested publication barrier"); @@ -213,6 +258,7 @@ fn nested_publication_barrier_precedes_already_queued_delivery() { subscribers: vec![nested_subscriber], scope_stack: nested_scope_stack, publication_context: None, + lineage: None, }]) .unwrap(); event @@ -222,6 +268,7 @@ fn nested_publication_barrier_precedes_already_queued_delivery() { subscribers: vec![subscriber.clone()], scope_stack: current_scope_stack(), publication_context: None, + lineage: None, }) .unwrap(); started_rx @@ -235,6 +282,7 @@ fn nested_publication_barrier_precedes_already_queued_delivery() { subscribers: vec![subscriber], scope_stack: current_scope_stack(), publication_context: None, + lineage: None, }) .unwrap(); release_tx.send(()).unwrap(); @@ -245,6 +293,131 @@ fn nested_publication_barrier_precedes_already_queued_delivery() { ); } +#[test] +fn flush_waits_for_transitive_subscriber_publications_without_reordering() { + let _lock = crate::shared_runtime::runtime_owner_test_mutex() + .lock() + .unwrap_or_else(|error| error.into_inner()); + flush_subscribers().unwrap(); + let sender = dispatcher_sender().expect("dispatcher sender"); + let delivered = Arc::new(Mutex::new(Vec::new())); + let event = |uuid: &str, name: &str| { + serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": uuid, + "timestamp": "2026-07-28T00:00:00Z", + "name": name + })) + .expect("valid event") + }; + + let grandchild_subscriber: EventSubscriberFn = { + let delivered = Arc::clone(&delivered); + Arc::new(move |event| { + delivered + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(event.name().to_string()); + }) + }; + let child_subscriber: EventSubscriberFn = { + let delivered = Arc::clone(&delivered); + let grandchild_subscriber = grandchild_subscriber.clone(); + Arc::new(move |event| { + delivered + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(event.name().to_string()); + assert!(enqueue_dispatch_message(DispatcherMessage::Deliver { + event: Box::new( + serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000010", + "timestamp": "2026-07-28T00:00:00Z", + "name": "grandchild" + })) + .expect("valid event"), + ), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![grandchild_subscriber.clone()], + scope_stack: current_scope_stack(), + publication_context: None, + lineage: None, + })); + }) + }; + let outer_subscriber: EventSubscriberFn = { + let delivered = Arc::clone(&delivered); + let child_subscriber = child_subscriber.clone(); + Arc::new(move |event| { + delivered + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(event.name().to_string()); + assert!(enqueue_dispatch_message(DispatcherMessage::Deliver { + event: Box::new( + serde_json::from_value(serde_json::json!({ + "kind": "mark", + "atof_version": "0.1", + "uuid": "019c1df6-4a57-7000-8000-000000000009", + "timestamp": "2026-07-28T00:00:00Z", + "name": "child" + })) + .expect("valid event"), + ), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![child_subscriber.clone()], + scope_stack: current_scope_stack(), + publication_context: None, + lineage: None, + })); + }) + }; + let later_subscriber: EventSubscriberFn = { + let delivered = Arc::clone(&delivered); + Arc::new(move |event| { + delivered + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(event.name().to_string()); + }) + }; + + sender + .send(DispatcherMessage::Deliver { + event: Box::new(event("019c1df6-4a57-7000-8000-000000000008", "outer")), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![outer_subscriber], + scope_stack: current_scope_stack(), + publication_context: None, + lineage: None, + }) + .unwrap(); + sender + .send(DispatcherMessage::Deliver { + event: Box::new(event("019c1df6-4a57-7000-8000-000000000011", "later")), + transform: None, + sanitizers: Vec::new(), + subscribers: vec![later_subscriber], + scope_stack: current_scope_stack(), + publication_context: None, + lineage: None, + }) + .unwrap(); + + flush_subscribers().unwrap(); + assert_eq!( + *delivered.lock().unwrap_or_else(|error| error.into_inner()), + ["outer", "later", "child", "grandchild"], + "subscriber publications retain FIFO position and one flush waits for all descendants" + ); +} + #[test] fn detached_publications_share_one_background_executor_thread() { let _lock = crate::shared_runtime::runtime_owner_test_mutex() diff --git a/crates/ffi/nemo_relay.h b/crates/ffi/nemo_relay.h index 6517cfc2e..9a17fe485 100644 --- a/crates/ffi/nemo_relay.h +++ b/crates/ffi/nemo_relay.h @@ -1279,7 +1279,8 @@ NemoRelayStatus nemo_relay_register_subscriber(const char *name, NemoRelayStatus nemo_relay_deregister_subscriber(const char *name); /** - * Wait for subscriber callbacks queued before this call to finish. + * Wait for subscriber callbacks queued before this call and events emitted + * transitively by those callbacks to finish. * * Call this function outside native subscriber callbacks. A re-entrant call returns without * waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can diff --git a/crates/ffi/src/api/llm_registry.rs b/crates/ffi/src/api/llm_registry.rs index 38883ecee..5c9ba7420 100644 --- a/crates/ffi/src/api/llm_registry.rs +++ b/crates/ffi/src/api/llm_registry.rs @@ -389,7 +389,8 @@ pub unsafe extern "C" fn nemo_relay_deregister_subscriber(name: *const c_char) - } } -/// Wait for subscriber callbacks queued before this call to finish. +/// Wait for subscriber callbacks queued before this call and events emitted +/// transitively by those callbacks to finish. /// /// Call this function outside native subscriber callbacks. A re-entrant call returns without /// waiting to avoid blocking the dispatcher, so callbacks later in the same dispatch snapshot can diff --git a/crates/node/README.md b/crates/node/README.md index ea6486462..0de18dc68 100644 --- a/crates/node/README.md +++ b/crates/node/README.md @@ -100,9 +100,11 @@ main().catch((error) => { ``` Native subscriber delivery is asynchronous. Awaiting `flushSubscribers()` drains -the native dispatcher without blocking the Node.js event loop. The extra -event-loop turn lets queued JavaScript callback side effects complete before -deregistration or exit. +the native dispatcher without blocking the Node.js event loop. JavaScript +subscribers run later through Node's callback queue, so native events they emit +are separate publications. The extra event-loop turn lets queued JavaScript +callback side effects complete before deregistration or exit; flush again if +those side effects emit native events that must also be observed. The main runtime API is exported from `nemo-relay-node`. Additional entry points are available at `nemo-relay-node/typed`, `nemo-relay-node/plugin`, diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index 7689c900c..7eadb340a 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -3242,6 +3242,8 @@ pub fn deregister_subscriber(name: String) -> Result { /// /// JavaScript subscribers are queued through Node's `ThreadsafeFunction`. Awaiting this /// Promise does not block the Node event loop while Promise-returning event sanitizers settle. +/// Native events emitted later by a JavaScript subscriber are separate publications and may +/// require another flush after the JavaScript callback runs. /// /// The Promise rejects if the blocking task fails or the core subscriber flush returns an error. /// Callers should handle errors when awaiting it. diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index 5ca8097fc..9d37efa45 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -1562,7 +1562,7 @@ fn deregister_subscriber(name: &str) -> PyResult { core_subscriber_api::deregister_subscriber(name).map_err(to_py_err) } -/// Wait for subscriber callbacks queued before this call to finish. +/// Wait for queued subscriber callbacks and their transitive native publications. /// /// Call this function outside subscribers, event sanitizers, conditional /// guardrails, and request or execution intercepts. The public Python wrapper diff --git a/go/nemo_relay/nemo_relay.go b/go/nemo_relay/nemo_relay.go index 978e40228..a3e80fd18 100644 --- a/go/nemo_relay/nemo_relay.go +++ b/go/nemo_relay/nemo_relay.go @@ -1542,12 +1542,12 @@ func DeregisterSubscriber(name string) error { return checkStatus(C.nemo_relay_deregister_subscriber(cName)) } -// FlushSubscribers waits for subscriber callbacks queued before this call to -// finish. Native event-producing APIs enqueue subscriber work and return -// without waiting for observer callbacks. Call this function outside native -// subscriber callbacks. A re-entrant call returns without waiting to avoid -// blocking the dispatcher, so callbacks later in the same dispatch snapshot -// can still run. +// FlushSubscribers waits for subscriber callbacks queued before this call and +// events emitted transitively by those callbacks to finish. Native +// event-producing APIs enqueue subscriber work and return without waiting for +// observer callbacks. Call this function outside native subscriber callbacks. +// A re-entrant call returns without waiting to avoid blocking the dispatcher, +// so callbacks later in the same dispatch snapshot can still run. func FlushSubscribers() error { return checkStatus(C.nemo_relay_flush_subscribers()) } diff --git a/python/nemo_relay/_native.pyi b/python/nemo_relay/_native.pyi index 5b52de253..512d311c8 100644 --- a/python/nemo_relay/_native.pyi +++ b/python/nemo_relay/_native.pyi @@ -1976,7 +1976,7 @@ def deregister_subscriber(name: str) -> bool: ... def flush_subscribers() -> None: - """Wait for subscriber callbacks queued by native event emission. + """Wait for queued subscriber callbacks and their transitive publications. Call this function outside subscribers, event sanitizers, conditional guardrails, and request or execution intercepts. The public Python wrapper diff --git a/python/nemo_relay/subscribers.py b/python/nemo_relay/subscribers.py index 9f305fe38..92c9baabe 100644 --- a/python/nemo_relay/subscribers.py +++ b/python/nemo_relay/subscribers.py @@ -187,7 +187,7 @@ def deregister(name: str) -> bool: def flush() -> None: - """Wait for subscriber callbacks already queued by native event emission. + """Wait for queued subscriber callbacks and their transitive publications. Native NeMo Relay event APIs enqueue subscriber callbacks and return without waiting for observer work. Use this barrier in tests and shutdown paths when @@ -217,7 +217,7 @@ def flush() -> None: async def flush_async() -> None: - """Wait asynchronously for subscriber callbacks already queued by Relay. + """Wait asynchronously for queued callbacks and transitive publications. Use this barrier from an ``asyncio`` task. A process-local daemon bridge thread coalesces concurrent barriers and waits for the native dispatcher From 2d24d3ceb5e2da11b912dba6f8bd3684cdbe56f5 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 17:28:41 -0400 Subject: [PATCH 63/83] chore: refresh Rust attributions after rebase Signed-off-by: Will Killian --- ATTRIBUTIONS-Rust.md | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index ee60e8a33..a9df3ef75 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -26115,9 +26115,8 @@ SOFTWARE. ## md-5 - 0.11.0 **Repository URL**: https://github.com/RustCrypto/hashes -**License Type(s)**: MIT OR Apache-2.0 -### License: https://spdx.org/licenses/ -### License File: LICENSE-APACHE +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 @@ -26322,38 +26321,6 @@ See the License for the specific language governing permissions and limitations under the License. ``` -### License File: LICENSE-MIT -``` -Copyright (c) 2016-2026 The RustCrypto Project Developers -Copyright (c) 2016 Artyom Pavlov -Copyright (c) 2009-2013 Mozilla Foundation -Copyright (c) 2006-2009 Graydon Hoare - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -``` - ## memchr - 2.8.0 **Repository URL**: https://github.com/BurntSushi/memchr **License Type(s)**: MIT From 92706ae955707149b8f6cdf40e94ae595411efa0 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 29 Jul 2026 17:47:59 -0400 Subject: [PATCH 64/83] fix(ci): align Rust attributions with pinned generator Signed-off-by: Will Killian --- ATTRIBUTIONS-Rust.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index a9df3ef75..ee60e8a33 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -26115,8 +26115,9 @@ SOFTWARE. ## md-5 - 0.11.0 **Repository URL**: https://github.com/RustCrypto/hashes -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html +**License Type(s)**: MIT OR Apache-2.0 +### License: https://spdx.org/licenses/ +### License File: LICENSE-APACHE ``` Apache License Version 2.0, January 2004 @@ -26321,6 +26322,38 @@ See the License for the specific language governing permissions and limitations under the License. ``` +### License File: LICENSE-MIT +``` +Copyright (c) 2016-2026 The RustCrypto Project Developers +Copyright (c) 2016 Artyom Pavlov +Copyright (c) 2009-2013 Mozilla Foundation +Copyright (c) 2006-2009 Graydon Hoare + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` + ## memchr - 2.8.0 **Repository URL**: https://github.com/BurntSushi/memchr **License Type(s)**: MIT From 3db987ce4b18663803d5da44a64ff1d66c14d9be Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 15:02:27 -0700 Subject: [PATCH 65/83] fix(node): require Rampart PII selectors Signed-off-by: Alex Fournier --- crates/node/pii_rampart.d.ts | 5 ++++- crates/node/pii_rampart.js | 13 +++++++++++-- crates/node/tests/pii_rampart_tests.mjs | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/node/pii_rampart.d.ts b/crates/node/pii_rampart.d.ts index ed33e9b71..318efefe9 100644 --- a/crates/node/pii_rampart.d.ts +++ b/crates/node/pii_rampart.d.ts @@ -27,10 +27,13 @@ export interface Config { policy?: ConfigPolicy; } +export type ConfigWithSelectors = Partial & + ({ target_paths: string[] } | { target_path_patterns: string[] }); + export declare const RAMPART_PII_PLUGIN_KIND: 'pii_rampart'; export declare const RAMPART_MODEL_ID: 'nationaldesignstudio/rampart'; export declare const RAMPART_MODEL_REVISION: 'b1993e4e68b082835b80ffc65acc03325ea2e501'; -export declare function defaultConfig(modelPath: string, config?: Partial): Config; +export declare function defaultConfig(modelPath: string, config: ConfigWithSelectors): Config; export declare function ComponentSpec( config: Config, options?: { enabled?: boolean }, diff --git a/crates/node/pii_rampart.js b/crates/node/pii_rampart.js index 1cc6d7ab7..93a4d7b66 100644 --- a/crates/node/pii_rampart.js +++ b/crates/node/pii_rampart.js @@ -12,11 +12,20 @@ const RAMPART_MODEL_REVISION = 'b1993e4e68b082835b80ffc65acc03325ea2e501'; /** * Create Rampart PII settings with runtime defaults applied. * + * At least one exact path or path pattern is required because Relay does not + * send unselected observability fields to the model. + * * @param {string} modelPath - Absolute path to the pinned Rampart snapshot. - * @param {object} [config={}] - Partial settings to override. + * @param {object} config - Partial settings including explicit target selectors. * @returns {object} A normalized Rampart PII config object. */ -function defaultConfig(modelPath, config = {}) { +function defaultConfig(modelPath, config) { + const hasTargetPaths = Array.isArray(config?.target_paths) && config.target_paths.length > 0; + const hasTargetPathPatterns = + Array.isArray(config?.target_path_patterns) && config.target_path_patterns.length > 0; + if (!hasTargetPaths && !hasTargetPathPatterns) { + throw new TypeError('Rampart PII config requires target_paths or target_path_patterns'); + } return { version: 1, model_path: modelPath, diff --git a/crates/node/tests/pii_rampart_tests.mjs b/crates/node/tests/pii_rampart_tests.mjs index 1605580c2..60ae5d9a9 100644 --- a/crates/node/tests/pii_rampart_tests.mjs +++ b/crates/node/tests/pii_rampart_tests.mjs @@ -21,6 +21,31 @@ describe('pii_rampart plugin helpers', () => { const component = rampart.ComponentSpec(config); assert.equal(component.kind, rampart.RAMPART_PII_PLUGIN_KIND); assert.equal(component.enabled, true); + assert.deepEqual(rampart.validateConfig(config).diagnostics, []); + }); + + it('requires explicit selectors in the config helper', () => { + const exact = rampart.defaultConfig('/models/rampart', { + target_paths: ['/message'], + }); + assert.deepEqual(rampart.validateConfig(exact).diagnostics, []); + + assert.throws( + () => rampart.defaultConfig('/models/rampart'), + /requires target_paths or target_path_patterns/, + ); + assert.throws( + () => rampart.defaultConfig('/models/rampart', {}), + /requires target_paths or target_path_patterns/, + ); + assert.throws( + () => + rampart.defaultConfig('/models/rampart', { + target_paths: [], + target_path_patterns: [], + }), + /requires target_paths or target_path_patterns/, + ); }); it('is registered and validates malformed paths', () => { From 8c43b12691a6ffc5b62147ad3479b79542f45850 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 15:43:29 -0700 Subject: [PATCH 66/83] fix(pii): omit unsafe multi-choice Rampart responses Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/sanitizer.rs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index de6c82f52..cbee2e083 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -374,12 +374,35 @@ impl RampartSanitizer { surface: ProviderSurface, payload: Json, ) -> Option { + if surface == ProviderSurface::OpenAIChat + && payload + .get("choices") + .and_then(Json::as_array) + .is_some_and(|choices| choices.len() > 1) + && self.targets_normalized_openai_chat_choice() + { + return None; + } let codec_name = BuiltinCodecName::from_provider_surface(surface); let annotated = codec.decode_response(&payload).ok()?; let sanitized = sanitize_serializable(self, annotated).ok()?; Some(codec_name.overlay_response_payload(payload, &sanitized)) } + fn targets_normalized_openai_chat_choice(&self) -> bool { + const CHOICE_ROOTS: [&str; 4] = ["message", "tool_calls", "finish_reason", "api_specific"]; + + self.target_paths + .iter() + .filter_map(|path| path.first()) + .chain( + self.target_path_patterns + .iter() + .filter_map(|pattern| pattern.segments.first()), + ) + .any(|root| root == "*" || CHOICE_ROOTS.contains(&root.as_str())) + } + fn selected_surface(&self, codec: &LlmCodecIdentity) -> Option { match codec { LlmCodecIdentity::None => self.legacy_surface, @@ -734,4 +757,78 @@ mod tests { assert_eq!(sanitized["model"], "model-José"); assert_eq!(sanitized["vendor_trace"], "trace-José"); } + + #[test] + fn openai_chat_response_projection_omits_multiple_choices_for_choice_targets() { + let exact = RampartSanitizer::new( + RampartPiiConfig { + model_path: "/tmp/rampart".into(), + target_paths: vec!["/message".into()], + ..RampartPiiConfig::default() + }, + Arc::new(NameDetector), + ) + .unwrap(); + let wildcard = sanitizer(Arc::new(NameDetector), vec!["/*"]); + let payload = serde_json::json!({ + "id": "chatcmpl-multi", + "model": "model", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello José"}, + "finish_reason": "stop" + }, + { + "index": 1, + "message": {"role": "assistant", "content": "Private José"}, + "finish_reason": "stop" + } + ] + }); + let codec = build_response_codec(ProviderSurface::OpenAIChat); + + assert!( + exact + .sanitize_response_with_codec( + codec.as_ref(), + ProviderSurface::OpenAIChat, + payload.clone(), + ) + .is_none() + ); + assert!( + wildcard + .sanitize_response_with_codec(codec.as_ref(), ProviderSurface::OpenAIChat, payload) + .is_none() + ); + } + + #[test] + fn openai_chat_response_projection_keeps_multiple_choices_for_response_targets() { + let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/model"]); + let payload = serde_json::json!({ + "id": "chatcmpl-multi", + "model": "model-José", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "first"}, + "finish_reason": "stop" + }, + { + "index": 1, + "message": {"role": "assistant", "content": "second"}, + "finish_reason": "stop" + } + ] + }); + let codec = build_response_codec(ProviderSurface::OpenAIChat); + let sanitized = sanitizer + .sanitize_response_with_codec(codec.as_ref(), ProviderSurface::OpenAIChat, payload) + .unwrap(); + + assert_eq!(sanitized["model"], "model-[REDACTED]"); + assert_eq!(sanitized["choices"][1]["message"]["content"], "second"); + } } From 512b80035c8bea680f3cde0c3c533cebb101613e Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 15:57:18 -0700 Subject: [PATCH 67/83] fix(pii): enforce Rampart activation invariants Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/mod.rs | 183 +++++++++++++++--------- 1 file changed, 119 insertions(+), 64 deletions(-) diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs index e76e91141..911de9a3d 100644 --- a/crates/pii-redaction/src/rampart/mod.rs +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -231,6 +231,7 @@ impl Plugin for RampartPiiPlugin { let parsed = parse_config(plugin_config); Box::pin(async move { let config = parsed?; + enforce_activation_invariants(&config)?; let model_path = PathBuf::from(&config.model_path); let max_windows = config.max_windows_per_payload; let batch_size = config.inference_batch_size; @@ -410,99 +411,121 @@ fn validate_rampart_pii_config( } } - if config.version != default_config_version() { + for violation in config_value_violations(&config) { push_unsupported( &mut diagnostics, &config, + violation.field, + violation.message, + ); + } + diagnostics +} + +struct ConfigViolation { + field: &'static str, + message: String, +} + +impl ConfigViolation { + fn new(field: &'static str, message: impl Into) -> Self { + Self { + field, + message: message.into(), + } + } +} + +fn enforce_activation_invariants(config: &RampartPiiConfig) -> PluginResult<()> { + let violations = config_value_violations(config); + if violations.is_empty() { + return Ok(()); + } + let details = violations + .into_iter() + .map(|violation| format!("{}: {}", violation.field, violation.message)) + .collect::>() + .join("; "); + Err(PluginError::InvalidConfig(format!( + "invalid Rampart PII plugin config: {details}" + ))) +} + +fn config_value_violations(config: &RampartPiiConfig) -> Vec { + let mut violations = Vec::new(); + if config.version != default_config_version() { + violations.push(ConfigViolation::new( "version", format!( "Rampart PII config version {} is unsupported", config.version ), - ); + )); } if config.model_path.trim().is_empty() || config.model_path.len() > MAX_MODEL_PATH_BYTES { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "model_path", format!("model_path must be non-empty and at most {MAX_MODEL_PATH_BYTES} UTF-8 bytes"), - ); + )); } else if !PathBuf::from(&config.model_path).is_absolute() { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "model_path", - "model_path must be absolute".into(), - ); + "model_path must be absolute", + )); } if !(config.input || config.output || config.mark || config.tool_input || config.tool_output) { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "input", - "at least one sanitization surface must be enabled".into(), - ); + "at least one sanitization surface must be enabled", + )); } if let Some(codec) = config.codec.as_deref() && !supported_codec_names().contains(&codec) { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "codec", - "codec must be 'openai_chat', 'openai_responses', or 'anthropic_messages'".into(), - ); + "codec must be 'openai_chat', 'openai_responses', or 'anthropic_messages'", + )); } if config.target_paths.is_empty() && config.target_path_patterns.is_empty() { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "target_paths", - "target_paths or target_path_patterns must select explicit content fields".into(), - ); + "target_paths or target_path_patterns must select explicit content fields", + )); } if config.target_paths.len() + config.target_path_patterns.len() > MAX_TARGET_PATHS { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "target_paths", format!( "target_paths and target_path_patterns must contain at most {MAX_TARGET_PATHS} entries" ), - ); + )); } if config .target_paths .iter() .any(|path| path.len() > MAX_TARGET_PATH_BYTES || !is_valid_json_pointer(path)) { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "target_paths", - "target_paths entries must be bounded valid JSON pointers".into(), - ); + "target_paths entries must be bounded valid JSON pointers", + )); } if config .target_path_patterns .iter() .any(|path| path.len() > MAX_TARGET_PATH_BYTES || !is_valid_json_pointer_pattern(path)) { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "target_path_patterns", - "target_path_patterns entries must be bounded JSON pointers with only complete '*' segments".into(), - ); + "target_path_patterns entries must be bounded JSON pointers with only complete '*' segments", + )); } if !config.min_score.is_finite() || !(0.0..=1.0).contains(&config.min_score) { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "min_score", - "min_score must be a finite number between 0 and 1".into(), - ); + "min_score must be a finite number between 0 and 1", + )); } if config.excluded_labels.len() > MAX_EXCLUDED_LABELS || config @@ -512,40 +535,32 @@ fn validate_rampart_pii_config( || config.excluded_labels.iter().collect::>().len() != config.excluded_labels.len() { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "excluded_labels", format!( "excluded_labels must contain at most {MAX_EXCLUDED_LABELS} unique, bounded labels" ), - ); + )); } if config.replacement.len() > MAX_REPLACEMENT_BYTES { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "replacement", format!("replacement must not exceed {MAX_REPLACEMENT_BYTES} UTF-8 bytes"), - ); + )); } if !(1..=512).contains(&config.max_windows_per_payload) { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "max_windows_per_payload", - "max_windows_per_payload must be between 1 and 512".into(), - ); + "max_windows_per_payload must be between 1 and 512", + )); } if !(1..=64).contains(&config.inference_batch_size) { - push_unsupported( - &mut diagnostics, - &config, + violations.push(ConfigViolation::new( "inference_batch_size", - "inference_batch_size must be between 1 and 64".into(), - ); + "inference_batch_size must be between 1 and 64", + )); } - diagnostics + violations } fn push_unsupported( @@ -689,6 +704,46 @@ mod tests { ); } + #[tokio::test] + async fn registration_enforces_safety_invariants_when_policy_warns() { + let cases = [ + ( + "target_paths", + serde_json::json!(["messages/0/content"]), + "target_paths entries", + ), + ("min_score", serde_json::json!(1.1), "min_score must"), + ]; + + for (field, value, expected) in cases { + let mut config = valid_config(); + config.insert( + "policy".into(), + serde_json::json!({"unsupported_value": "warn"}), + ); + config.insert(field.into(), value); + let diagnostics = validate_rampart_pii_config(&config, None); + assert!( + diagnostics.iter().any(|diagnostic| { + diagnostic.level == DiagnosticLevel::Warning + && diagnostic.field.as_deref() == Some(field) + }), + "expected a warning for {field}: {diagnostics:?}" + ); + + let plugin = RampartPiiPlugin; + let mut context = PluginRegistrationContext::with_namespace("rampart-test::"); + let error = plugin + .register(&config, &mut context) + .await + .expect_err("unsafe configuration must fail registration"); + assert!( + error.to_string().contains(expected), + "unexpected registration error for {field}: {error}" + ); + } + } + #[test] fn component_spec_uses_independent_plugin_kind() { let spec: PluginComponentSpec = ComponentSpec::new(RampartPiiConfig { From aa7dda605a65532793ff69e45eb894cb162e431a Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 16:30:29 -0700 Subject: [PATCH 68/83] fix(pii): apply Rampart structured prefilter Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/mod.rs | 1 + crates/pii-redaction/src/rampart/model.rs | 103 ++++- crates/pii-redaction/src/rampart/prefilter.rs | 398 ++++++++++++++++++ 3 files changed, 491 insertions(+), 11 deletions(-) create mode 100644 crates/pii-redaction/src/rampart/prefilter.rs diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs index 911de9a3d..4c317eb71 100644 --- a/crates/pii-redaction/src/rampart/mod.rs +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as Json}; mod model; +mod prefilter; mod sanitizer; mod tokenizer; diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index 46e9f0407..2583110bb 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -13,6 +13,7 @@ use serde::Deserialize; use sha2::{Digest, Sha256}; use tract_onnx::prelude::*; +use super::prefilter::PreparedText; use super::tokenizer::RampartTokenizer; const MODEL_MAX_TOKENS: usize = 512; @@ -95,6 +96,13 @@ struct Span { end: usize, label: String, score: f64, + source: SpanSource, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum SpanSource { + Model, + Deterministic, } struct SpanAccumulator { @@ -112,6 +120,7 @@ impl SpanAccumulator { end: self.end, label: self.label, score: self.score_total / self.token_count as f64, + source: SpanSource::Model, } } } @@ -189,17 +198,37 @@ impl RampartDetector { let _guard = self.inference_lock.lock().map_err(|error| { PluginError::Internal(format!("Rampart inference lock poisoned: {error}")) })?; - let windows = self.build_windows(texts)?; + let prepared = texts + .iter() + .map(|text| PreparedText::new(text)) + .collect::>(); + let masked_texts = prepared + .iter() + .map(PreparedText::masked) + .collect::>(); + let windows = self.build_windows(&masked_texts)?; if windows.is_empty() { - return Ok(Vec::new()); + return Ok(prepared + .iter() + .enumerate() + .flat_map(|(text_index, text)| { + text.spans().iter().map(move |span| Detection { + text_index, + start_utf8: span.start, + end_utf8: span.end, + label: span.label.into(), + score: 1.0, + }) + }) + .collect()); } - let mut spans_by_text = vec![Vec::new(); texts.len()]; + let mut model_spans_by_text = vec![Vec::new(); texts.len()]; for batch in inference_batches(&windows, self.inference_batch_size) { let logits = self.infer_batch(&windows, &batch)?; for (batch_index, window_index) in batch.iter().copied().enumerate() { let window = &windows[window_index]; - spans_by_text[window.text_index].extend(self.decode_window( + model_spans_by_text[window.text_index].extend(self.decode_window( window, &logits, batch_index, @@ -208,7 +237,28 @@ impl RampartDetector { } let mut detections = Vec::new(); - for (text_index, spans) in spans_by_text.into_iter().enumerate() { + for (text_index, model_spans) in model_spans_by_text.into_iter().enumerate() { + let mut spans = prepared[text_index] + .spans() + .iter() + .map(|span| Span { + start: span.start, + end: span.end, + label: span.label.into(), + score: 1.0, + source: SpanSource::Deterministic, + }) + .collect::>(); + for mut span in model_spans { + let Some((start, end)) = prepared[text_index].project(span.start, span.end) else { + return Err(inference_error( + "Rampart prefilter returned an invalid UTF-8 span", + )); + }; + span.start = start; + span.end = end; + spans.push(span); + } for span in merge_overlapping_spans(spans) { let text = texts[text_index]; if span.start >= span.end @@ -510,12 +560,17 @@ fn merge_overlapping_spans(mut spans: Vec) -> Vec { merged.push(span); continue; } - let span_wins = (span.score, span.end - span.start, span.label.as_str()) - > ( - previous.score, - previous.end - previous.start, - previous.label.as_str(), - ); + let span_wins = ( + span.score, + span.end - span.start, + span.source, + span.label.as_str(), + ) > ( + previous.score, + previous.end - previous.start, + previous.source, + previous.label.as_str(), + ); previous.start = previous.start.min(span.start); previous.end = previous.end.max(span.end); previous.score = previous.score.max(span.score); @@ -640,24 +695,28 @@ mod tests { end: 10, label: "GIVEN_NAME".into(), score: 0.8, + source: SpanSource::Model, }, Span { start: 5, end: 12, label: "SURNAME".into(), score: 0.9, + source: SpanSource::Model, }, Span { start: 20, end: 24, label: "PHONE".into(), score: 0.7, + source: SpanSource::Model, }, Span { start: 24, end: 28, label: "PHONE".into(), score: 0.8, + source: SpanSource::Model, }, ]); assert_eq!(merged.len(), 2); @@ -666,6 +725,28 @@ mod tests { assert_eq!((merged[1].start, merged[1].end), (20, 28)); } + #[test] + fn deterministic_span_wins_an_equal_model_tie() { + let merged = merge_overlapping_spans(vec![ + Span { + start: 0, + end: 11, + label: "GOVERNMENT_ID".into(), + score: 1.0, + source: SpanSource::Model, + }, + Span { + start: 0, + end: 11, + label: "SSN".into(), + score: 1.0, + source: SpanSource::Deterministic, + }, + ]); + assert_eq!(merged.len(), 1); + assert_eq!(merged[0].label, "SSN"); + } + #[test] fn confidence_uses_stable_softmax_and_rejects_non_finite_logits() { let logits = tract_ndarray::Array3::from_shape_vec((1, 1, 3), vec![0.0, 2.0, 1.0]).unwrap(); diff --git a/crates/pii-redaction/src/rampart/prefilter.rs b/crates/pii-redaction/src/rampart/prefilter.rs new file mode 100644 index 000000000..ce91bb24a --- /dev/null +++ b/crates/pii-redaction/src/rampart/prefilter.rs @@ -0,0 +1,398 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Structured prefilter required by the pinned Rampart model's input contract. + +use std::cmp::Reverse; +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::str::FromStr; +use std::sync::LazyLock; + +use regex::Regex; + +static EMAIL: LazyLock = LazyLock::new(|| { + Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b") + .expect("Rampart email pattern must compile") +}); +static SCHEME_URL: LazyLock = LazyLock::new(|| { + Regex::new(r#"\bhttps?://[^\s<>"'\])}]+"#).expect("Rampart URL pattern must compile") +}); +static WWW_URL: LazyLock = LazyLock::new(|| { + Regex::new(r#"\bwww\.[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?:/[^\s<>"'\])}]*)?"#) + .expect("Rampart www URL pattern must compile") +}); +static IPV4_CANDIDATE: LazyLock = LazyLock::new(|| { + Regex::new(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b").expect("Rampart IPv4 pattern must compile") +}); +static MAC_ADDRESS: LazyLock = LazyLock::new(|| { + Regex::new(r"\b(?:(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}|(?:[0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2})\b") + .expect("Rampart MAC address pattern must compile") +}); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) struct StructuredSpan { + pub(super) start: usize, + pub(super) end: usize, + pub(super) label: &'static str, +} + +pub(super) struct PreparedText { + masked: String, + raw_starts: Vec, + raw_ends: Vec, + spans: Vec, +} + +impl PreparedText { + pub(super) fn new(raw: &str) -> Self { + let spans = merge_spans(detect_structured(raw)); + let mut masked = String::with_capacity(raw.len()); + let mut raw_starts = Vec::with_capacity(raw.len()); + let mut raw_ends = Vec::with_capacity(raw.len()); + let mut cursor = 0; + + for span in &spans { + copy_verbatim( + raw, + cursor, + span.start, + &mut masked, + &mut raw_starts, + &mut raw_ends, + ); + let sentinel = format!("[{}]", span.label); + masked.push_str(&sentinel); + raw_starts.extend(std::iter::repeat_n(span.start, sentinel.len())); + raw_ends.extend(std::iter::repeat_n(span.end, sentinel.len())); + cursor = span.end; + } + copy_verbatim( + raw, + cursor, + raw.len(), + &mut masked, + &mut raw_starts, + &mut raw_ends, + ); + + debug_assert_eq!(masked.len(), raw_starts.len()); + debug_assert_eq!(masked.len(), raw_ends.len()); + Self { + masked, + raw_starts, + raw_ends, + spans, + } + } + + pub(super) fn masked(&self) -> &str { + &self.masked + } + + pub(super) fn spans(&self) -> &[StructuredSpan] { + &self.spans + } + + pub(super) fn project(&self, start: usize, end: usize) -> Option<(usize, usize)> { + if start >= end || end > self.masked.len() { + return None; + } + let raw_start = *self.raw_starts.get(start)?; + let raw_end = *self.raw_ends.get(end - 1)?; + (raw_start < raw_end).then_some((raw_start, raw_end)) + } +} + +fn copy_verbatim( + raw: &str, + start: usize, + end: usize, + masked: &mut String, + raw_starts: &mut Vec, + raw_ends: &mut Vec, +) { + masked.push_str(&raw[start..end]); + raw_starts.extend(start..end); + raw_ends.extend((start + 1)..=end); +} + +fn detect_structured(raw: &str) -> Vec { + let mut spans = detect_digit_entities(raw); + detect_regex_entities(raw, "EMAIL", &EMAIL, &mut spans); + detect_regex_entities(raw, "URL", &SCHEME_URL, &mut spans); + detect_regex_entities(raw, "URL", &WWW_URL, &mut spans); + for candidate in IPV4_CANDIDATE.find_iter(raw) { + if Ipv4Addr::from_str(candidate.as_str()).is_ok() { + spans.push(StructuredSpan { + start: candidate.start(), + end: candidate.end(), + label: "IP_ADDRESS", + }); + } + } + detect_ipv6(raw, &mut spans); + detect_regex_entities(raw, "IP_ADDRESS", &MAC_ADDRESS, &mut spans); + spans +} + +fn detect_digit_entities(raw: &str) -> Vec { + let bytes = raw.as_bytes(); + let mut spans = Vec::new(); + let mut cursor = 0; + while cursor < bytes.len() { + if !bytes[cursor].is_ascii_digit() { + cursor += 1; + continue; + } + + let start = cursor; + let mut digits = Vec::new(); + digits.push(bytes[cursor]); + cursor += 1; + while cursor < bytes.len() { + if bytes[cursor].is_ascii_digit() { + digits.push(bytes[cursor]); + cursor += 1; + continue; + } + if matches!(bytes[cursor], b' ' | b'.' | b'-') + && bytes.get(cursor + 1).is_some_and(u8::is_ascii_digit) + { + cursor += 1; + digits.push(bytes[cursor]); + cursor += 1; + continue; + } + break; + } + + let label = if matches!(digits.len(), 14..=16) && is_luhn_valid(&digits) { + Some("CREDIT_CARD") + } else if is_valid_ssn(&digits) { + Some("SSN") + } else { + None + }; + if let Some(label) = label { + spans.push(StructuredSpan { + start, + end: cursor, + label, + }); + } + } + spans +} + +fn is_luhn_valid(digits: &[u8]) -> bool { + let mut sum = 0_u32; + let mut double = false; + for digit in digits.iter().rev() { + let mut value = u32::from(*digit - b'0'); + if double { + value *= 2; + if value > 9 { + value -= 9; + } + } + sum += value; + double = !double; + } + sum.rem_euclid(10) == 0 +} + +fn is_valid_ssn(digits: &[u8]) -> bool { + if digits.len() != 9 { + return false; + } + let area = u16::from(digits[0] - b'0') * 100 + + u16::from(digits[1] - b'0') * 10 + + u16::from(digits[2] - b'0'); + area != 0 && area != 666 && area < 900 && digits[3..5] != *b"00" && digits[5..] != *b"0000" +} + +fn detect_regex_entities( + raw: &str, + label: &'static str, + pattern: &Regex, + spans: &mut Vec, +) { + spans.extend(pattern.find_iter(raw).map(|found| StructuredSpan { + start: found.start(), + end: found.end(), + label, + })); +} + +fn detect_ipv6(raw: &str, spans: &mut Vec) { + let mut start = None; + for (index, value) in raw.char_indices().chain(std::iter::once((raw.len(), '\0'))) { + if value.is_ascii_hexdigit() || matches!(value, ':' | '.') { + start.get_or_insert(index); + continue; + } + let Some(candidate_start) = start.take() else { + continue; + }; + let candidate = &raw[candidate_start..index]; + if candidate.contains(':') + && Ipv6Addr::from_str(candidate).is_ok() + && has_network_boundaries(raw, candidate_start, index) + { + spans.push(StructuredSpan { + start: candidate_start, + end: index, + label: "IP_ADDRESS", + }); + } + } +} + +fn has_network_boundaries(raw: &str, start: usize, end: usize) -> bool { + let invalid_boundary = |value: char| value.is_alphanumeric() || matches!(value, ':' | '.'); + !raw[..start] + .chars() + .next_back() + .is_some_and(invalid_boundary) + && !raw[end..].chars().next().is_some_and(invalid_boundary) +} + +fn merge_spans(mut spans: Vec) -> Vec { + spans.sort_by_key(|span| (span.start, Reverse(span.end), span.label)); + let mut merged: Vec = Vec::new(); + for span in spans { + let Some(previous) = merged.last_mut() else { + merged.push(span); + continue; + }; + if span.start >= previous.end { + merged.push(span); + continue; + } + + let previous_length = previous.end - previous.start; + let span_length = span.end - span.start; + if span_length > previous_length + || (span_length == previous_length && span.label < previous.label) + { + previous.label = span.label; + } + previous.start = previous.start.min(span.start); + previous.end = previous.end.max(span.end); + } + merged +} + +#[cfg(test)] +mod tests { + use super::*; + + fn labels_and_text(raw: &str) -> Vec<(&'static str, &str)> { + merge_spans(detect_structured(raw)) + .into_iter() + .map(|span| (span.label, &raw[span.start..span.end])) + .collect() + } + + #[test] + fn detects_validator_backed_identifiers_and_rejects_lookalikes() { + for value in ["472-81-0094", "472 81 0094", "472.81.0094", "472810094"] { + assert_eq!(labels_and_text(value), [("SSN", value)]); + } + for value in [ + "000-81-0094", + "666-81-0094", + "900-81-0094", + "472-00-0094", + "472-81-0000", + ] { + assert!(labels_and_text(value).is_empty(), "{value}"); + } + + assert_eq!( + labels_and_text("card 4111 1111 1111 1111"), + [("CREDIT_CARD", "4111 1111 1111 1111")] + ); + assert_eq!( + labels_and_text("card 378282246310005"), + [("CREDIT_CARD", "378282246310005")] + ); + assert_eq!( + labels_and_text("card 30569309025904"), + [("CREDIT_CARD", "30569309025904")] + ); + assert!(labels_and_text("card 1234 5678 1234 5678").is_empty()); + } + + #[test] + fn detects_text_and_network_identifiers_with_valid_boundaries() { + let raw = "mail alex+home@sub.example.org, visit https://example.org/private, \ + www.example.net/account, then use 10.0.0.1, 2001:db8::1, \ + fe80::1ff:fe23:4567:890a, ::1, \ + 2001:0db8:85a3:0000:0000:8a2e:0370:7334, \ + 00:1B:44:11:3A:B7, or 00-1B-44-11-3A-B7"; + assert_eq!( + labels_and_text(raw), + [ + ("EMAIL", "alex+home@sub.example.org"), + ("URL", "https://example.org/private,"), + ("URL", "www.example.net/account,"), + ("IP_ADDRESS", "10.0.0.1"), + ("IP_ADDRESS", "2001:db8::1"), + ("IP_ADDRESS", "fe80::1ff:fe23:4567:890a"), + ("IP_ADDRESS", "::1"), + ("IP_ADDRESS", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + ("IP_ADDRESS", "00:1B:44:11:3A:B7"), + ("IP_ADDRESS", "00-1B-44-11-3A-B7"), + ] + ); + assert!( + labels_and_text( + "invalid 999.0.0.1, time 12:34:56, opcode ff:00, \ + phone 415-555-2671, and address 31 Birchwood Avenue" + ) + .is_empty() + ); + } + + #[test] + fn premask_preserves_utf8_projection_and_structured_offsets() { + let raw = "José 472-81-0094 met Ana"; + let prepared = PreparedText::new(raw); + assert_eq!(prepared.masked(), "José [SSN] met Ana"); + assert_eq!( + prepared.spans(), + &[StructuredSpan { + start: 6, + end: 17, + label: "SSN", + }] + ); + + let sentinel_start = prepared.masked().find("[SSN]").unwrap(); + assert_eq!( + prepared.project(sentinel_start + 1, sentinel_start + 4), + Some((6, 17)) + ); + let ana_start = prepared.masked().find("Ana").unwrap(); + assert_eq!( + prepared.project(ana_start, ana_start + "Ana".len()), + Some((22, 25)) + ); + } + + #[test] + fn overlapping_structured_matches_produce_one_typed_sentinel() { + let raw = "open https://10.0.0.1/private"; + let prepared = PreparedText::new(raw); + assert_eq!(prepared.masked(), "open [URL]"); + assert_eq!( + prepared.spans(), + &[StructuredSpan { + start: 5, + end: raw.len(), + label: "URL", + }] + ); + } +} From 1f51a096cad6ae69b503b49b225440d76a8d99d2 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 16:39:20 -0700 Subject: [PATCH 69/83] fix(pii): match Rampart clean-text normalization Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/tokenizer.rs | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/crates/pii-redaction/src/rampart/tokenizer.rs b/crates/pii-redaction/src/rampart/tokenizer.rs index 6224038d7..24db06a83 100644 --- a/crates/pii-redaction/src/rampart/tokenizer.rs +++ b/crates/pii-redaction/src/rampart/tokenizer.rs @@ -187,10 +187,11 @@ fn normalize(text: &str, base_offset: usize) -> Vec { let original_end = original_start + original.len_utf8(); // Rampart's model card recommends splitting hyphenated identifiers. // Replacing one ASCII byte preserves the original byte offsets. + if original == '\0' || original == '\u{fffd}' || is_control(original) { + continue; + } let cleaned = if original == '-' || is_whitespace(original) { ' ' - } else if original == '\0' || original == '\u{fffd}' || original.is_other() { - continue; } else { original }; @@ -248,6 +249,10 @@ fn is_whitespace(value: char) -> bool { matches!(value, '\t' | '\n' | '\r') || value.is_whitespace() } +fn is_control(value: char) -> bool { + !matches!(value, '\t' | '\n' | '\r') && value.is_other() +} + fn is_chinese_char(value: char) -> bool { matches!( value as u32, @@ -272,8 +277,19 @@ mod tests { fn tokenizer() -> RampartTokenizer { let tokens = [ - "[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]", "hello", ",", "jose", "##ph", "野", - "alice", "rivera", + "[PAD]", + "[UNK]", + "[CLS]", + "[SEP]", + "[MASK]", + "hello", + ",", + "jose", + "##ph", + "野", + "alice", + "rivera", + "alicerivera", ]; let vocab = tokens .into_iter() @@ -324,6 +340,42 @@ mod tests { assert_eq!(encoded.offsets, [(0, 10), (13, 19)]); } + #[test] + fn removes_bert_controls_before_whitespace_normalization() { + for control in ['\u{000b}', '\u{000c}', '\u{0085}'] { + let text = format!("Alice{control}Rivera"); + let encoded = tokenizer().encode(&text).unwrap(); + assert_eq!(encoded.ids, [12], "control U+{:04X}", control as u32); + assert_eq!( + encoded.offsets, + [(0, text.len())], + "control U+{:04X}", + control as u32 + ); + } + } + + #[test] + fn keeps_bert_whitespace_as_token_separators() { + for whitespace in ['\t', '\n', '\r', '\u{2003}', '\u{2028}', '\u{2029}'] { + let text = format!("Alice{whitespace}Rivera"); + let separator_end = 5 + whitespace.len_utf8(); + let encoded = tokenizer().encode(&text).unwrap(); + assert_eq!( + encoded.ids, + [10, 11], + "whitespace U+{:04X}", + whitespace as u32 + ); + assert_eq!( + encoded.offsets, + [(0, 5), (separator_end, text.len())], + "whitespace U+{:04X}", + whitespace as u32 + ); + } + } + #[test] fn isolates_unicode_punctuation_and_chinese_characters() { let encoded = tokenizer().encode("Alice’野").unwrap(); From 07ce807fdcb13903e76038119ee5afb3dc18e544 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 16:53:19 -0700 Subject: [PATCH 70/83] docs(pii): document Rampart setup Signed-off-by: Alex Fournier --- crates/pii-redaction/README.md | 97 ++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index 4c8528582..65976f005 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -6,10 +6,13 @@ SPDX-License-Identifier: Apache-2.0 # NeMo Relay PII Redaction `nemo-relay-pii-redaction` is the first-party NeMo Relay plugin crate for -deterministic privacy redaction on tool and LLM observability payloads. It -ships the `pii_redaction` plugin contract, a production-ready `builtin` -backend, and the future `local_model` seam for model-backed detection and -redaction. +privacy redaction on tool and LLM observability payloads. It provides two +independent component kinds: + +- `pii_redaction` provides deterministic policies and a `local_model` + integration seam. +- `pii_rampart` optionally runs the pinned `nationaldesignstudio/rampart` + ONNX model inside the Relay process. The plugin is designed for the common case where teams want a supported, config-driven privacy policy surface instead of writing custom sanitize @@ -38,6 +41,8 @@ NeMo Relay PII Redaction allows you to: tool-call identity, model attribution, routing, usage, and cost analytics. - Use the `local_model` config contract and provider registration surface for future model-backed implementations. +- Enable the `rampart` feature to use the separate in-process `pii_rampart` + component for contextual PII detection. ## Plugin Versus Raw Middleware @@ -61,6 +66,12 @@ Install the plugin crate alongside the core runtime: cargo add nemo-relay nemo-relay-pii-redaction ``` +For a Rust application that uses the Rampart component, enable its feature: + +```bash +cargo add nemo-relay-pii-redaction --features rampart +``` + For local source development: ```bash @@ -196,6 +207,84 @@ without redesigning the public plugin surface. If `mode = "local_model"` is configured today, the runtime expects a registered local backend provider and fails fast if one is not installed. +## Rampart PII Component + +`pii_rampart` is a separate component, not an implementation of the +`pii_redaction.local_model` seam. It runs the pinned Rampart ONNX graph through +`tract-onnx` in the Relay Rust process. Relay does not download the model or +make network requests during activation. + +### Provision the Model + +Install the +[Hugging Face Hub CLI](https://huggingface.co/docs/huggingface_hub/en/guides/cli), +then download the files accepted by Relay into a deployment-owned directory: + +```bash +hf download nationaldesignstudio/rampart \ + config.json \ + onnx/model_q4.onnx \ + special_tokens_map.json \ + tokenizer.json \ + tokenizer_config.json \ + vocab.txt \ + --revision b1993e4e68b082835b80ffc65acc03325ea2e501 \ + --local-dir /absolute/path/to/rampart +``` + +Set `model_path` to that absolute directory. During activation, Relay verifies +the SHA-256 digest of every required file and rejects missing files or digest +mismatches before installing sanitizer callbacks. + +### Activate the Component + +Add a `pii_rampart` component with explicit content selectors. This example +sanitizes normalized LLM request and response content without sending marks, +tool payloads, or provider metadata to the model: + +```toml +[[components]] +kind = "pii_rampart" +enabled = true + +[components.config] +version = 1 +model_path = "/absolute/path/to/rampart" +codec = "openai_chat" +input = true +output = true +mark = false +tool_input = false +tool_output = false +target_paths = ["/message"] +target_path_patterns = [ + "/messages/*/content", + "/messages/*/content/*/text", +] +``` + +`target_paths` contains exact JSON pointers. `target_path_patterns` also +accepts `*` as one complete path segment. At least one selector is required. +When a supported codec is active, selectors address the normalized Relay +request or response shape. + +### Use a Language Binding + +The CLI, Python, Node.js, and FFI host entry points register `pii_rampart` +automatically. Rust applications that initialize plugin configuration directly +must enable the `rampart` feature and call +`register_rampart_pii_component()` first. + +Configuration helpers are available through these binding modules: + +- Rust: `nemo_relay_pii_redaction::rampart` +- Python: `nemo_relay.pii_rampart` +- Node.js: `nemo-relay-node/pii_rampart` +- Go: `github.com/NVIDIA/NeMo-Relay/go/nemo_relay/pii_rampart` + +Go and the raw C FFI remain experimental and source-first. Model loading and +inference run in the shared Rust implementation for every binding. + ## Documentation [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay) From 79a861de7794b782cb538220dbaece51b49fcb73 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 17:30:39 -0700 Subject: [PATCH 71/83] fix(pii): avoid blocking executors during Rampart inference Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/model.rs | 43 ++- crates/pii-redaction/src/rampart/sanitizer.rs | 309 ++++++++++++------ 2 files changed, 246 insertions(+), 106 deletions(-) diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index 2583110bb..47ad7a6b7 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -6,7 +6,7 @@ use std::fmt::Write as _; use std::fs::File; use std::io::{BufReader, Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, MutexGuard, TryLockError}; use nemo_relay::plugin::{PluginError, Result as PluginResult}; use serde::Deserialize; @@ -67,8 +67,9 @@ pub(super) struct RampartDetector { pad_id: i64, max_windows_per_payload: usize, inference_batch_size: usize, - // Tokenization and inference share one admission point to bound aggregate - // request-local tensor memory under concurrent Relay traffic. + // Tokenization and inference share one non-waiting admission point. Async + // sanitizer callbacks fail closed on contention instead of occupying + // blocking threads while another inference is running. inference_lock: Mutex<()>, } @@ -195,9 +196,7 @@ impl RampartDetector { } pub(super) fn detect(&self, texts: &[&str]) -> PluginResult> { - let _guard = self.inference_lock.lock().map_err(|error| { - PluginError::Internal(format!("Rampart inference lock poisoned: {error}")) - })?; + let _guard = try_inference_guard(&self.inference_lock)?; let prepared = texts .iter() .map(|text| PreparedText::new(text)) @@ -645,6 +644,17 @@ fn required_verified_file( }) } +fn try_inference_guard(lock: &Mutex<()>) -> PluginResult> { + lock.try_lock().map_err(|error| match error { + TryLockError::WouldBlock => { + PluginError::Internal("Rampart inference is already running".into()) + } + TryLockError::Poisoned(error) => { + PluginError::Internal(format!("Rampart inference lock poisoned: {error}")) + } + }) +} + fn invalid_model(message: impl Into) -> PluginError { PluginError::InvalidConfig(message.into()) } @@ -657,6 +667,8 @@ fn inference_error(message: impl Into) -> PluginError { mod tests { use super::*; use std::fs; + use std::sync::mpsc; + use std::time::Duration; #[test] fn batches_bound_padded_token_volume() { @@ -687,6 +699,25 @@ mod tests { })); } + #[test] + fn inference_admission_does_not_wait_for_the_active_model() { + let lock = Arc::new(Mutex::new(())); + let active = lock.lock().unwrap(); + let contender = Arc::clone(&lock); + let (result_tx, result_rx) = mpsc::channel(); + std::thread::spawn(move || { + let result = try_inference_guard(&contender).map(|_| ()); + result_tx.send(result).unwrap(); + }); + + let error = result_rx + .recv_timeout(Duration::from_millis(100)) + .expect("contending inference admission should not wait") + .unwrap_err(); + assert!(error.to_string().contains("already running")); + drop(active); + } + #[test] fn overlap_merge_is_deterministic() { let merged = merge_overlapping_spans(vec![ diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index cbee2e083..c6103acd6 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -15,6 +15,7 @@ use nemo_relay::codec::resolve::{ response_codec as build_response_codec, }; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; +use nemo_relay::error::{FlowError, Result as FlowResult}; use nemo_relay::plugin::{PluginError, Result as PluginResult}; use serde::Serialize; use serde::de::DeserializeOwned; @@ -444,7 +445,12 @@ impl RampartSanitizer { } pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeFn { - Arc::new(move |_name, payload| backend.sanitize_json(payload)) + Arc::new(move |_name, payload| { + let backend = backend.clone(); + Box::pin(async move { + run_blocking("tool payload", move || backend.sanitize_json(payload)).await + }) + }) } pub(super) fn event_sanitize_callback( @@ -452,119 +458,161 @@ pub(super) fn event_sanitize_callback( scope_categories: Option<(bool, bool)>, ) -> EventSanitizeFn { Arc::new(move |event, mut fields| { - if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { - matches!(event, Event::Scope(_)) - && event - .category() - .is_some_and(|category| match category.as_str() { - "llm" => !sanitize_llm, - "tool" => !sanitize_tool, - _ => false, - }) - }) { - return fields; - } - let specialized_scope = matches!(event, Event::Scope(_)) - && event - .category() - .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); - - let mut selected = Vec::with_capacity(3); - if !specialized_scope && let Some(data) = fields.data.take() { - selected.push((EventField::Data, data)); - } - if !specialized_scope - && let Some(profile) = fields.category_profile.take() - && let Ok(profile) = serde_json::to_value(profile) - { - selected.push((EventField::CategoryProfile, profile)); - } - if let Some(metadata) = fields.metadata.take() { - selected.push((EventField::Metadata, metadata)); - } + let backend = backend.clone(); + Box::pin(async move { + run_blocking("event fields", move || { + if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { + matches!(event.as_ref(), Event::Scope(_)) + && event + .category() + .is_some_and(|category| match category.as_str() { + "llm" => !sanitize_llm, + "tool" => !sanitize_tool, + _ => false, + }) + }) { + return fields; + } + let specialized_scope = matches!(event.as_ref(), Event::Scope(_)) + && event + .category() + .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); + + let mut selected = Vec::with_capacity(3); + if !specialized_scope && let Some(data) = fields.data.take() { + selected.push((EventField::Data, data)); + } + if !specialized_scope + && let Some(profile) = fields.category_profile.take() + && let Ok(profile) = serde_json::to_value(profile) + { + selected.push((EventField::CategoryProfile, profile)); + } + if let Some(metadata) = fields.metadata.take() { + selected.push((EventField::Metadata, metadata)); + } - let values = selected - .iter_mut() - .map(|(_, value)| std::mem::take(value)) - .collect(); - for ((field, _), value) in selected - .into_iter() - .zip(backend.sanitize_json_values(values)) - { - match field { - EventField::Data => fields.data = Some(value), - EventField::CategoryProfile => { - fields.category_profile = serde_json::from_value(value).ok(); + let values = selected + .iter_mut() + .map(|(_, value)| std::mem::take(value)) + .collect(); + for ((field, _), value) in selected + .into_iter() + .zip(backend.sanitize_json_values(values)) + { + match field { + EventField::Data => fields.data = Some(value), + EventField::CategoryProfile => { + fields.category_profile = serde_json::from_value(value).ok(); + } + EventField::Metadata => fields.metadata = Some(value), + } } - EventField::Metadata => fields.metadata = Some(value), - } - } - fields + fields + }) + .await + }) }) } pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSanitizeRequestFn { Arc::new(move |request, context| { - if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - return backend.sanitize_raw_request(request); - } - let resolved = context.resolve_codec(); - let fallback = if resolved.is_none() { - backend - .selected_surface(context.codec()) - .map(build_request_codec) - } else { - None - }; - let sanitized = resolved - .as_deref() - .or(fallback.as_deref()) - .and_then(|codec| backend.sanitize_request_with_codec(codec, &request)); - if sanitized.is_none() { - backend.log_codec_failure( - "request", - context.codec(), - "codec decode, sanitize, or encode failure", - ); - } - sanitized + let backend = backend.clone(); + Box::pin(async move { + run_blocking("LLM request", move || { + if matches!(context.codec(), LlmCodecIdentity::None) + && backend.legacy_surface.is_none() + { + return backend.sanitize_raw_request(request); + } + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + backend + .selected_surface(context.codec()) + .map(build_request_codec) + } else { + None + }; + let sanitized = resolved + .as_deref() + .or(fallback.as_deref()) + .and_then(|codec| backend.sanitize_request_with_codec(codec, &request)); + if sanitized.is_none() { + backend.log_codec_failure( + "request", + context.codec(), + "codec decode, sanitize, or encode failure", + ); + } + sanitized + }) + .await + }) }) } pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSanitizeResponseFn { Arc::new(move |payload, context| { - if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - return Some(backend.sanitize_json(payload)); - } - if matches!(context.codec(), LlmCodecIdentity::None) - && !backend.uses_compatible_legacy_response_codec(&payload) - { - backend.log_codec_failure("response", context.codec(), "no compatible legacy codec"); - return None; - } - let surface = backend.selected_surface(context.codec()); - let resolved = context.resolve_codec(); - let fallback = if resolved.is_none() { - surface.map(build_response_codec) - } else { - None - }; - let sanitized = surface - .zip(resolved.as_deref().or(fallback.as_deref())) - .and_then(|(surface, codec)| { - backend.sanitize_response_with_codec(codec, surface, payload) - }); - if sanitized.is_none() { - backend.log_codec_failure( - "response", - context.codec(), - "codec decode, sanitize, or encode failure", - ); - } - sanitized + let backend = backend.clone(); + Box::pin(async move { + run_blocking("LLM response", move || { + if matches!(context.codec(), LlmCodecIdentity::None) + && backend.legacy_surface.is_none() + { + return Some(backend.sanitize_json(payload)); + } + if matches!(context.codec(), LlmCodecIdentity::None) + && !backend.uses_compatible_legacy_response_codec(&payload) + { + backend.log_codec_failure( + "response", + context.codec(), + "no compatible legacy codec", + ); + return None; + } + let surface = backend.selected_surface(context.codec()); + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + surface.map(build_response_codec) + } else { + None + }; + let sanitized = surface + .zip(resolved.as_deref().or(fallback.as_deref())) + .and_then(|(surface, codec)| { + backend.sanitize_response_with_codec(codec, surface, payload) + }); + if sanitized.is_none() { + backend.log_codec_failure( + "response", + context.codec(), + "codec decode, sanitize, or encode failure", + ); + } + sanitized + }) + .await + }) }) } +async fn run_blocking( + target: &'static str, + operation: impl FnOnce() -> T + Send + 'static, +) -> FlowResult +where + T: Send + 'static, +{ + tokio::task::spawn_blocking(operation) + .await + .map_err(|error| { + FlowError::Internal(format!( + "Rampart {target} sanitization task failed: {error}" + )) + }) +} + fn compile_json_pointer(pointer: String) -> Vec { pointer.strip_prefix('/').map_or_else(Vec::new, |path| { path.split('/').map(str::to_string).collect() @@ -581,7 +629,8 @@ where #[cfg(test)] mod tests { - use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::time::Duration; use super::*; @@ -622,6 +671,21 @@ mod tests { } } + struct BlockingDetector { + started: Arc, + release: Arc, + } + + impl DetectionModel for BlockingDetector { + fn detect(&self, _texts: &[&str]) -> PluginResult> { + self.started.store(true, Ordering::Release); + while !self.release.load(Ordering::Acquire) { + std::thread::sleep(Duration::from_millis(1)); + } + Ok(Vec::new()) + } + } + fn sanitizer(detector: Arc, patterns: Vec<&str>) -> RampartSanitizer { RampartSanitizer::new( RampartPiiConfig { @@ -706,6 +770,51 @@ mod tests { assert_eq!(calls.load(Ordering::Relaxed), 1); } + #[tokio::test(flavor = "current_thread")] + async fn async_callback_does_not_block_the_runtime_thread() { + let started = Arc::new(AtomicBool::new(false)); + let release = Arc::new(AtomicBool::new(false)); + let backend = sanitizer( + Arc::new(BlockingDetector { + started: Arc::clone(&started), + release: Arc::clone(&release), + }), + vec!["/message"], + ); + let callback = tool_sanitize_callback(backend); + let task = tokio::spawn(callback( + "tool".into(), + serde_json::json!({"message": "private"}), + )); + + tokio::time::timeout(Duration::from_secs(1), async { + while !started.load(Ordering::Acquire) { + tokio::task::yield_now().await; + } + }) + .await + .expect("blocking detector should start"); + + let heartbeat = Arc::new(AtomicUsize::new(0)); + let heartbeat_task = { + let heartbeat = Arc::clone(&heartbeat); + tokio::spawn(async move { + for _ in 0..4 { + tokio::time::sleep(Duration::from_millis(1)).await; + heartbeat.fetch_add(1, Ordering::Relaxed); + } + }) + }; + heartbeat_task.await.unwrap(); + assert_eq!(heartbeat.load(Ordering::Relaxed), 4); + + release.store(true, Ordering::Release); + assert_eq!( + task.await.unwrap().unwrap(), + serde_json::json!({"message": "private"}) + ); + } + #[test] fn openai_chat_request_projection_preserves_provider_fields() { let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/messages/*/content"]); From 65c3a16879467cbbcdcd83659e860bc1839ad898 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 18:37:02 -0700 Subject: [PATCH 72/83] fix(pii): bound concurrent Rampart inference Signed-off-by: Alex Fournier --- crates/pii-redaction/README.md | 8 + crates/pii-redaction/src/rampart/model.rs | 42 +-- crates/pii-redaction/src/rampart/sanitizer.rs | 307 ++++++++++++++++-- 3 files changed, 290 insertions(+), 67 deletions(-) diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index 65976f005..d03252c2a 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -285,6 +285,14 @@ Configuration helpers are available through these binding modules: Go and the raw C FFI remain experimental and source-first. Model loading and inference run in the shared Rust implementation for every binding. +Rampart admits at most two sanitizer operations before submitting work to +Tokio's blocking pool. Additional operations do not queue for model inference. +They fail closed immediately: tool observability payloads become the configured +replacement, LLM bodies are omitted, and mutable mark or generic scope fields +are omitted. Tool and LLM scope metadata is omitted independently so an +already-sanitized specialized payload remains available. These fallbacks do not +change the arguments or return values seen by the underlying tool or model. + ## Documentation [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay) diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index 47ad7a6b7..7ea792fe6 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -6,7 +6,7 @@ use std::fmt::Write as _; use std::fs::File; use std::io::{BufReader, Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex, MutexGuard, TryLockError}; +use std::sync::Arc; use nemo_relay::plugin::{PluginError, Result as PluginResult}; use serde::Deserialize; @@ -60,6 +60,8 @@ pub(super) struct Detection { pub(super) struct RampartDetector { tokenizer: RampartTokenizer, + // Each run creates invocation-local tract state; the sanitizer bounds how + // many callers may share this immutable plan concurrently. plan: Arc, labels: Arc<[String]>, cls_id: i64, @@ -67,10 +69,6 @@ pub(super) struct RampartDetector { pad_id: i64, max_windows_per_payload: usize, inference_batch_size: usize, - // Tokenization and inference share one non-waiting admission point. Async - // sanitizer callbacks fail closed on contention instead of occupying - // blocking threads while another inference is running. - inference_lock: Mutex<()>, } #[derive(Deserialize)] @@ -189,14 +187,12 @@ impl RampartDetector { pad_id, max_windows_per_payload, inference_batch_size, - inference_lock: Mutex::new(()), }; detector.detect(&["warmup"])?; Ok(detector) } pub(super) fn detect(&self, texts: &[&str]) -> PluginResult> { - let _guard = try_inference_guard(&self.inference_lock)?; let prepared = texts .iter() .map(|text| PreparedText::new(text)) @@ -644,17 +640,6 @@ fn required_verified_file( }) } -fn try_inference_guard(lock: &Mutex<()>) -> PluginResult> { - lock.try_lock().map_err(|error| match error { - TryLockError::WouldBlock => { - PluginError::Internal("Rampart inference is already running".into()) - } - TryLockError::Poisoned(error) => { - PluginError::Internal(format!("Rampart inference lock poisoned: {error}")) - } - }) -} - fn invalid_model(message: impl Into) -> PluginError { PluginError::InvalidConfig(message.into()) } @@ -667,8 +652,6 @@ fn inference_error(message: impl Into) -> PluginError { mod tests { use super::*; use std::fs; - use std::sync::mpsc; - use std::time::Duration; #[test] fn batches_bound_padded_token_volume() { @@ -699,25 +682,6 @@ mod tests { })); } - #[test] - fn inference_admission_does_not_wait_for_the_active_model() { - let lock = Arc::new(Mutex::new(())); - let active = lock.lock().unwrap(); - let contender = Arc::clone(&lock); - let (result_tx, result_rx) = mpsc::channel(); - std::thread::spawn(move || { - let result = try_inference_guard(&contender).map(|_| ()); - result_tx.send(result).unwrap(); - }); - - let error = result_rx - .recv_timeout(Duration::from_millis(100)) - .expect("contending inference admission should not wait") - .unwrap_err(); - assert!(error.to_string().contains("already running")); - drop(active); - } - #[test] fn overlap_merge_is_deterministic() { let merged = merge_overlapping_spans(vec![ diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index c6103acd6..06eec968c 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -3,8 +3,9 @@ use std::collections::HashSet; use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; -use nemo_relay::api::event::Event; +use nemo_relay::api::event::{Event, EventSanitizeFields}; use nemo_relay::api::llm::LlmRequest; use nemo_relay::api::runtime::{ BuiltinLlmCodec, EventSanitizeFn, LlmCodecIdentity, LlmSanitizeRequestFn, @@ -30,6 +31,9 @@ use super::model::{Detection, RampartDetector}; const MAX_TEXT_BYTES: usize = 16 * 1024; const MAX_TEXTS_PER_PAYLOAD: usize = 256; const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; +// Bound parallelism without multiplying worst-case request-local tensor memory. +// Additional observability work fails closed instead of queueing. +const MAX_CONCURRENT_INFERENCE: usize = 2; pub(super) trait DetectionModel: Send + Sync { fn detect(&self, texts: &[&str]) -> PluginResult>; @@ -50,6 +54,7 @@ pub(super) struct RampartSanitizer { excluded_labels: Arc>, replacement: Arc, legacy_surface: Option, + admission: Arc, } #[derive(Clone)] @@ -85,6 +90,16 @@ enum EventField { Metadata, } +struct SanitizerPermit { + admission: Arc, +} + +impl Drop for SanitizerPermit { + fn drop(&mut self) { + self.admission.fetch_sub(1, Ordering::Release); + } +} + impl RampartSanitizer { pub(super) fn new( config: RampartPiiConfig, @@ -116,9 +131,37 @@ impl RampartSanitizer { excluded_labels: Arc::new(config.excluded_labels.into_iter().collect()), replacement: config.replacement.into(), legacy_surface, + admission: Arc::new(AtomicUsize::new(0)), + }) + } + + fn try_admit(&self, surface: &'static str) -> Option { + if self + .admission + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |active| { + (active < MAX_CONCURRENT_INFERENCE).then_some(active + 1) + }) + .is_err() + { + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + reason = "contention", + surface; + "Rampart PII sanitization failed closed before blocking admission" + ); + return None; + } + Some(SanitizerPermit { + admission: Arc::clone(&self.admission), }) } + fn fail_closed_payload(&self) -> Json { + Json::String(self.replacement.to_string()) + } + fn sanitize_json(&self, value: Json) -> Json { self.sanitize_json_values(vec![value]) .pop() @@ -447,8 +490,15 @@ impl RampartSanitizer { pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeFn { Arc::new(move |_name, payload| { let backend = backend.clone(); + let Some(permit) = backend.try_admit("tool") else { + let payload = backend.fail_closed_payload(); + return Box::pin(async move { Ok(payload) }); + }; Box::pin(async move { - run_blocking("tool payload", move || backend.sanitize_json(payload)).await + run_blocking("tool payload", permit, move || { + backend.sanitize_json(payload) + }) + .await }) }) } @@ -459,24 +509,19 @@ pub(super) fn event_sanitize_callback( ) -> EventSanitizeFn { Arc::new(move |event, mut fields| { let backend = backend.clone(); + if skips_event_sanitization(event.as_ref(), scope_categories) { + return Box::pin(async move { Ok(fields) }); + } + if !event_has_candidate_fields(event.as_ref(), &fields) { + return Box::pin(async move { Ok(fields) }); + } + let Some(permit) = backend.try_admit("event") else { + let fields = fail_closed_event_fields(event.as_ref(), fields); + return Box::pin(async move { Ok(fields) }); + }; Box::pin(async move { - run_blocking("event fields", move || { - if scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { - matches!(event.as_ref(), Event::Scope(_)) - && event - .category() - .is_some_and(|category| match category.as_str() { - "llm" => !sanitize_llm, - "tool" => !sanitize_tool, - _ => false, - }) - }) { - return fields; - } - let specialized_scope = matches!(event.as_ref(), Event::Scope(_)) - && event - .category() - .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")); + run_blocking("event fields", permit, move || { + let specialized_scope = is_specialized_scope(event.as_ref()); let mut selected = Vec::with_capacity(3); if !specialized_scope && let Some(data) = fields.data.take() { @@ -518,8 +563,11 @@ pub(super) fn event_sanitize_callback( pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSanitizeRequestFn { Arc::new(move |request, context| { let backend = backend.clone(); + let Some(permit) = backend.try_admit("llm_request") else { + return Box::pin(async move { Ok(None) }); + }; Box::pin(async move { - run_blocking("LLM request", move || { + run_blocking("LLM request", permit, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { @@ -554,8 +602,11 @@ pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSan pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSanitizeResponseFn { Arc::new(move |payload, context| { let backend = backend.clone(); + let Some(permit) = backend.try_admit("llm_response") else { + return Box::pin(async move { Ok(None) }); + }; Box::pin(async move { - run_blocking("LLM response", move || { + run_blocking("LLM response", permit, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { @@ -599,18 +650,59 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa async fn run_blocking( target: &'static str, + permit: SanitizerPermit, operation: impl FnOnce() -> T + Send + 'static, ) -> FlowResult where T: Send + 'static, { - tokio::task::spawn_blocking(operation) - .await - .map_err(|error| { - FlowError::Internal(format!( - "Rampart {target} sanitization task failed: {error}" - )) - }) + tokio::task::spawn_blocking(move || { + let _permit = permit; + operation() + }) + .await + .map_err(|error| { + FlowError::Internal(format!( + "Rampart {target} sanitization task failed: {error}" + )) + }) +} + +fn skips_event_sanitization(event: &Event, scope_categories: Option<(bool, bool)>) -> bool { + scope_categories.is_some_and(|(sanitize_llm, sanitize_tool)| { + matches!(event, Event::Scope(_)) + && event + .category() + .is_some_and(|category| match category.as_str() { + "llm" => !sanitize_llm, + "tool" => !sanitize_tool, + _ => false, + }) + }) +} + +fn fail_closed_event_fields(event: &Event, mut fields: EventSanitizeFields) -> EventSanitizeFields { + if is_specialized_scope(event) { + fields.metadata = None; + fields + } else { + EventSanitizeFields::default() + } +} + +fn event_has_candidate_fields(event: &Event, fields: &EventSanitizeFields) -> bool { + if is_specialized_scope(event) { + fields.metadata.is_some() + } else { + fields.data.is_some() || fields.category_profile.is_some() || fields.metadata.is_some() + } +} + +fn is_specialized_scope(event: &Event) -> bool { + matches!(event, Event::Scope(_)) + && event + .category() + .is_some_and(|category| matches!(category.as_str(), "tool" | "llm")) } fn compile_json_pointer(pointer: String) -> Vec { @@ -674,6 +766,7 @@ mod tests { struct BlockingDetector { started: Arc, release: Arc, + finished: Arc, } impl DetectionModel for BlockingDetector { @@ -682,6 +775,7 @@ mod tests { while !self.release.load(Ordering::Acquire) { std::thread::sleep(Duration::from_millis(1)); } + self.finished.store(true, Ordering::Release); Ok(Vec::new()) } } @@ -774,10 +868,12 @@ mod tests { async fn async_callback_does_not_block_the_runtime_thread() { let started = Arc::new(AtomicBool::new(false)); let release = Arc::new(AtomicBool::new(false)); + let finished = Arc::new(AtomicBool::new(false)); let backend = sanitizer( Arc::new(BlockingDetector { started: Arc::clone(&started), release: Arc::clone(&release), + finished, }), vec!["/message"], ); @@ -815,6 +911,161 @@ mod tests { ); } + #[test] + fn admission_bounds_work_queued_for_the_blocking_pool() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .max_blocking_threads(1) + .build() + .unwrap(); + runtime.block_on(async { + let started = Arc::new(AtomicBool::new(false)); + let release = Arc::new(AtomicBool::new(false)); + let finished = Arc::new(AtomicBool::new(false)); + let backend = sanitizer( + Arc::new(BlockingDetector { + started: Arc::clone(&started), + release: Arc::clone(&release), + finished, + }), + vec!["/message"], + ); + let admission = Arc::clone(&backend.admission); + let callback = tool_sanitize_callback(backend); + let active = tokio::spawn(callback( + "active".into(), + serde_json::json!({"message": "private"}), + )); + tokio::time::timeout(Duration::from_secs(1), async { + while !started.load(Ordering::Acquire) { + tokio::task::yield_now().await; + } + }) + .await + .expect("blocking detector should start"); + + let queued = tokio::spawn(callback( + "queued".into(), + serde_json::json!({"message": "private"}), + )); + assert_eq!(admission.load(Ordering::Acquire), 2); + + let overloaded = tokio::time::timeout( + Duration::from_millis(100), + callback( + "overloaded".into(), + serde_json::json!({"message": "private", "metadata": "visible"}), + ), + ) + .await + .expect("overloaded sanitizer should not enter the blocking pool") + .unwrap(); + assert_eq!(overloaded, Json::String("[REDACTED]".into())); + + release.store(true, Ordering::Release); + assert_eq!( + active.await.unwrap().unwrap(), + serde_json::json!({"message": "private"}) + ); + assert_eq!( + queued.await.unwrap().unwrap(), + serde_json::json!({"message": "private"}) + ); + }); + } + + #[tokio::test(flavor = "current_thread")] + async fn cancelled_callback_keeps_admission_until_blocking_work_finishes() { + let started = Arc::new(AtomicBool::new(false)); + let release = Arc::new(AtomicBool::new(false)); + let finished = Arc::new(AtomicBool::new(false)); + let backend = sanitizer( + Arc::new(BlockingDetector { + started: Arc::clone(&started), + release: Arc::clone(&release), + finished: Arc::clone(&finished), + }), + vec!["/message"], + ); + let admission = Arc::clone(&backend.admission); + let callback = tool_sanitize_callback(backend); + let active = tokio::spawn(callback( + "active".into(), + serde_json::json!({"message": "private"}), + )); + tokio::time::timeout(Duration::from_secs(1), async { + while !started.load(Ordering::Acquire) { + tokio::task::yield_now().await; + } + }) + .await + .expect("blocking detector should start"); + + active.abort(); + assert!(active.await.unwrap_err().is_cancelled()); + let admitted = tokio::spawn(callback( + "admitted".into(), + serde_json::json!({"message": "private"}), + )); + assert_eq!(admission.load(Ordering::Acquire), 2); + assert_eq!( + callback( + "overloaded".into(), + serde_json::json!({"message": "private"}), + ) + .await + .unwrap(), + Json::String("[REDACTED]".into()) + ); + + release.store(true, Ordering::Release); + assert_eq!( + admitted.await.unwrap().unwrap(), + serde_json::json!({"message": "private"}) + ); + tokio::time::timeout(Duration::from_secs(1), async { + while !finished.load(Ordering::Acquire) || admission.load(Ordering::Acquire) != 0 { + tokio::task::yield_now().await; + } + }) + .await + .expect("detached blocking work should finish"); + assert_eq!( + callback( + "recovered".into(), + serde_json::json!({"message": "private"}), + ) + .await + .unwrap(), + serde_json::json!({"message": "private"}) + ); + } + + #[test] + fn empty_specialized_scope_does_not_require_admission() { + use nemo_relay::api::event::{ + BaseEvent, CategoryProfile, EventCategory, ScopeCategory, ScopeEvent, + }; + + let event = Event::Scope(ScopeEvent::new( + BaseEvent::builder() + .name("tool") + .data(serde_json::json!({"message": "[REDACTED]"})) + .build(), + ScopeCategory::Start, + Default::default(), + EventCategory::tool(), + Some(CategoryProfile::default()), + )); + let fields = event.sanitize_fields(); + assert!(!event_has_candidate_fields(&event, &fields)); + assert_eq!( + fail_closed_event_fields(&event, fields.clone()), + fields, + "specialized data already handled by the tool sanitizer must be preserved" + ); + } + #[test] fn openai_chat_request_projection_preserves_provider_fields() { let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/messages/*/content"]); From e1a241d6dc1638432399fb96fbed0ec74240712d Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 18:51:26 -0700 Subject: [PATCH 73/83] fix(pii): keep Rampart inference single-flight Signed-off-by: Alex Fournier --- crates/pii-redaction/README.md | 4 +- crates/pii-redaction/src/rampart/model.rs | 4 +- crates/pii-redaction/src/rampart/sanitizer.rs | 52 +++++-------------- 3 files changed, 17 insertions(+), 43 deletions(-) diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index d03252c2a..3b4c0beae 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -285,8 +285,8 @@ Configuration helpers are available through these binding modules: Go and the raw C FFI remain experimental and source-first. Model loading and inference run in the shared Rust implementation for every binding. -Rampart admits at most two sanitizer operations before submitting work to -Tokio's blocking pool. Additional operations do not queue for model inference. +Rampart admits one sanitizer operation before submitting work to Tokio's +blocking pool. Concurrent operations do not queue for model inference. They fail closed immediately: tool observability payloads become the configured replacement, LLM bodies are omitted, and mutable mark or generic scope fields are omitted. Tool and LLM scope metadata is omitted independently so an diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index 7ea792fe6..fc676d471 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -60,8 +60,8 @@ pub(super) struct Detection { pub(super) struct RampartDetector { tokenizer: RampartTokenizer, - // Each run creates invocation-local tract state; the sanitizer bounds how - // many callers may share this immutable plan concurrently. + // Single-flight admission is enforced before this immutable plan enters + // Tokio's blocking pool. plan: Arc, labels: Arc<[String]>, cls_id: i64, diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index 06eec968c..9dbf69af2 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use nemo_relay::api::event::{Event, EventSanitizeFields}; use nemo_relay::api::llm::LlmRequest; @@ -31,10 +31,6 @@ use super::model::{Detection, RampartDetector}; const MAX_TEXT_BYTES: usize = 16 * 1024; const MAX_TEXTS_PER_PAYLOAD: usize = 256; const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; -// Bound parallelism without multiplying worst-case request-local tensor memory. -// Additional observability work fails closed instead of queueing. -const MAX_CONCURRENT_INFERENCE: usize = 2; - pub(super) trait DetectionModel: Send + Sync { fn detect(&self, texts: &[&str]) -> PluginResult>; } @@ -54,7 +50,7 @@ pub(super) struct RampartSanitizer { excluded_labels: Arc>, replacement: Arc, legacy_surface: Option, - admission: Arc, + admission: Arc, } #[derive(Clone)] @@ -91,12 +87,12 @@ enum EventField { } struct SanitizerPermit { - admission: Arc, + admission: Arc, } impl Drop for SanitizerPermit { fn drop(&mut self) { - self.admission.fetch_sub(1, Ordering::Release); + self.admission.store(false, Ordering::Release); } } @@ -131,16 +127,14 @@ impl RampartSanitizer { excluded_labels: Arc::new(config.excluded_labels.into_iter().collect()), replacement: config.replacement.into(), legacy_surface, - admission: Arc::new(AtomicUsize::new(0)), + admission: Arc::new(AtomicBool::new(false)), }) } fn try_admit(&self, surface: &'static str) -> Option { if self .admission - .fetch_update(Ordering::AcqRel, Ordering::Acquire, |active| { - (active < MAX_CONCURRENT_INFERENCE).then_some(active + 1) - }) + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .is_err() { log::warn!( @@ -912,7 +906,7 @@ mod tests { } #[test] - fn admission_bounds_work_queued_for_the_blocking_pool() { + fn contention_does_not_queue_behind_the_blocking_pool() { let runtime = tokio::runtime::Builder::new_current_thread() .enable_time() .max_blocking_threads(1) @@ -930,7 +924,6 @@ mod tests { }), vec!["/message"], ); - let admission = Arc::clone(&backend.admission); let callback = tool_sanitize_callback(backend); let active = tokio::spawn(callback( "active".into(), @@ -944,33 +937,23 @@ mod tests { .await .expect("blocking detector should start"); - let queued = tokio::spawn(callback( - "queued".into(), - serde_json::json!({"message": "private"}), - )); - assert_eq!(admission.load(Ordering::Acquire), 2); - - let overloaded = tokio::time::timeout( + let contending = tokio::time::timeout( Duration::from_millis(100), callback( - "overloaded".into(), + "contending".into(), serde_json::json!({"message": "private", "metadata": "visible"}), ), ) .await - .expect("overloaded sanitizer should not enter the blocking pool") + .expect("contending sanitizer should not enter the blocking pool") .unwrap(); - assert_eq!(overloaded, Json::String("[REDACTED]".into())); + assert_eq!(contending, Json::String("[REDACTED]".into())); release.store(true, Ordering::Release); assert_eq!( active.await.unwrap().unwrap(), serde_json::json!({"message": "private"}) ); - assert_eq!( - queued.await.unwrap().unwrap(), - serde_json::json!({"message": "private"}) - ); }); } @@ -1003,14 +986,9 @@ mod tests { active.abort(); assert!(active.await.unwrap_err().is_cancelled()); - let admitted = tokio::spawn(callback( - "admitted".into(), - serde_json::json!({"message": "private"}), - )); - assert_eq!(admission.load(Ordering::Acquire), 2); assert_eq!( callback( - "overloaded".into(), + "contending".into(), serde_json::json!({"message": "private"}), ) .await @@ -1019,12 +997,8 @@ mod tests { ); release.store(true, Ordering::Release); - assert_eq!( - admitted.await.unwrap().unwrap(), - serde_json::json!({"message": "private"}) - ); tokio::time::timeout(Duration::from_secs(1), async { - while !finished.load(Ordering::Acquire) || admission.load(Ordering::Acquire) != 0 { + while !finished.load(Ordering::Acquire) || admission.load(Ordering::Acquire) { tokio::task::yield_now().await; } }) From f078c391814148f2bebaaf9284babb0985e1df04 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Wed, 29 Jul 2026 18:53:47 -0700 Subject: [PATCH 74/83] fix(pii): fail closed when Rampart tasks panic Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/sanitizer.rs | 101 ++++++++++++++++-- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index 9dbf69af2..0111bf202 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -16,7 +16,7 @@ use nemo_relay::codec::resolve::{ response_codec as build_response_codec, }; use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; -use nemo_relay::error::{FlowError, Result as FlowResult}; +use nemo_relay::error::Result as FlowResult; use nemo_relay::plugin::{PluginError, Result as PluginResult}; use serde::Serialize; use serde::de::DeserializeOwned; @@ -488,8 +488,9 @@ pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeF let payload = backend.fail_closed_payload(); return Box::pin(async move { Ok(payload) }); }; + let fallback = backend.fail_closed_payload(); Box::pin(async move { - run_blocking("tool payload", permit, move || { + run_blocking("tool payload", permit, fallback, move || { backend.sanitize_json(payload) }) .await @@ -513,8 +514,9 @@ pub(super) fn event_sanitize_callback( let fields = fail_closed_event_fields(event.as_ref(), fields); return Box::pin(async move { Ok(fields) }); }; + let fallback = fail_closed_event_fields(event.as_ref(), fields.clone()); Box::pin(async move { - run_blocking("event fields", permit, move || { + run_blocking("event fields", permit, fallback, move || { let specialized_scope = is_specialized_scope(event.as_ref()); let mut selected = Vec::with_capacity(3); @@ -561,7 +563,7 @@ pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSan return Box::pin(async move { Ok(None) }); }; Box::pin(async move { - run_blocking("LLM request", permit, move || { + run_blocking("LLM request", permit, None, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { @@ -600,7 +602,7 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa return Box::pin(async move { Ok(None) }); }; Box::pin(async move { - run_blocking("LLM response", permit, move || { + run_blocking("LLM response", permit, None, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { @@ -645,21 +647,32 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa async fn run_blocking( target: &'static str, permit: SanitizerPermit, + fallback: T, operation: impl FnOnce() -> T + Send + 'static, ) -> FlowResult where T: Send + 'static, { - tokio::task::spawn_blocking(move || { + match tokio::task::spawn_blocking(move || { let _permit = permit; operation() }) .await - .map_err(|error| { - FlowError::Internal(format!( - "Rampart {target} sanitization task failed: {error}" - )) - }) + { + Ok(value) => Ok(value), + Err(error) => { + log::error!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + reason = "blocking_task", + target, + panicked = error.is_panic(); + "Rampart PII blocking sanitization failed closed: {error}" + ); + Ok(fallback) + } + } } fn skips_event_sanitization(event: &Event, scope_categories: Option<(bool, bool)>) -> bool { @@ -748,6 +761,14 @@ mod tests { } } + struct PanickingDetector; + + impl DetectionModel for PanickingDetector { + fn detect(&self, _texts: &[&str]) -> PluginResult> { + panic!("model panic") + } + } + struct CountingDetector(Arc); impl DetectionModel for CountingDetector { @@ -905,6 +926,64 @@ mod tests { ); } + #[tokio::test(flavor = "current_thread")] + async fn blocking_task_panics_fail_closed_for_every_surface() { + use nemo_relay::api::event::{BaseEvent, MarkEvent}; + use nemo_relay::api::runtime::{LlmSanitizeRequestContext, LlmSanitizeResponseContext}; + + let backend = sanitizer(Arc::new(PanickingDetector), vec!["/message"]); + let tool = tool_sanitize_callback(backend.clone()); + assert_eq!( + tool( + "tool".into(), + serde_json::json!({"message": "private", "metadata": "visible"}), + ) + .await + .unwrap(), + Json::String("[REDACTED]".into()) + ); + + let event = Arc::new(Event::Mark(MarkEvent::new( + BaseEvent::builder() + .name("mark") + .data(serde_json::json!({"message": "private"})) + .metadata(serde_json::json!({"message": "private"})) + .build(), + None, + None, + ))); + let fields = event.sanitize_fields(); + assert_eq!( + event_sanitize_callback(backend.clone(), None)(event, fields) + .await + .unwrap(), + EventSanitizeFields::default() + ); + + let request = LlmRequest { + headers: Map::new(), + content: serde_json::json!({"message": "private"}), + }; + assert!( + llm_sanitize_request_callback(backend.clone())( + request, + LlmSanitizeRequestContext::default(), + ) + .await + .unwrap() + .is_none() + ); + assert!( + llm_sanitize_response_callback(backend)( + serde_json::json!({"message": "private"}), + LlmSanitizeResponseContext::default(), + ) + .await + .unwrap() + .is_none() + ); + } + #[test] fn contention_does_not_queue_behind_the_blocking_pool() { let runtime = tokio::runtime::Builder::new_current_thread() From 3c98b43bf23cf67fbc30744e620dd9ec5826e5a8 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Thu, 30 Jul 2026 19:52:30 -0700 Subject: [PATCH 75/83] fix(pii): harden Rampart concurrency and config Signed-off-by: Alex Fournier --- .../tests/coverage/shared/plugins_tests.rs | 10 + crates/node/pii_rampart.d.ts | 12 +- crates/node/pii_rampart.js | 1 + crates/node/tests/pii_rampart_tests.mjs | 8 + crates/pii-redaction/Cargo.toml | 2 +- crates/pii-redaction/README.md | 11 +- crates/pii-redaction/src/rampart/model.rs | 5 +- crates/pii-redaction/src/rampart/sanitizer.rs | 414 +++++++++++++++--- crates/pii-redaction/src/rampart/tokenizer.rs | 17 +- 9 files changed, 403 insertions(+), 77 deletions(-) diff --git a/crates/cli/tests/coverage/shared/plugins_tests.rs b/crates/cli/tests/coverage/shared/plugins_tests.rs index 8f8edb07f..de1707a9e 100644 --- a/crates/cli/tests/coverage/shared/plugins_tests.rs +++ b/crates/cli/tests/coverage/shared/plugins_tests.rs @@ -1253,6 +1253,12 @@ fn editor_save_preserves_unknown_rampart_pii_fields_and_prunes_version() { json!("/srv/models/rampart"), ) .unwrap(); + set_struct_field( + &mut rampart.config, + "target_paths", + json!(["/message", "/metadata/note"]), + ) + .unwrap(); rampart.set_enabled(false); store_rampart_pii_state(&mut config, &rampart).unwrap(); @@ -1271,6 +1277,10 @@ fn editor_save_preserves_unknown_rampart_pii_fields_and_prunes_version() { component.config.get("future_top_level"), Some(&json!("preserve")) ); + assert_eq!( + component.config.get("target_paths"), + Some(&json!(["/message", "/metadata/note"])) + ); } #[test] diff --git a/crates/node/pii_rampart.d.ts b/crates/node/pii_rampart.d.ts index 318efefe9..8c06f7af4 100644 --- a/crates/node/pii_rampart.d.ts +++ b/crates/node/pii_rampart.d.ts @@ -27,8 +27,16 @@ export interface Config { policy?: ConfigPolicy; } -export type ConfigWithSelectors = Partial & - ({ target_paths: string[] } | { target_path_patterns: string[] }); +type NonEmptyStringArray = [string, ...string[]]; + +export type ConfigWithSelectors = Omit< + Partial, + 'model_path' | 'target_paths' | 'target_path_patterns' +> & + ( + | { target_paths: NonEmptyStringArray; target_path_patterns?: string[] } + | { target_paths?: string[]; target_path_patterns: NonEmptyStringArray } + ); export declare const RAMPART_PII_PLUGIN_KIND: 'pii_rampart'; export declare const RAMPART_MODEL_ID: 'nationaldesignstudio/rampart'; diff --git a/crates/node/pii_rampart.js b/crates/node/pii_rampart.js index 93a4d7b66..cbf423742 100644 --- a/crates/node/pii_rampart.js +++ b/crates/node/pii_rampart.js @@ -43,6 +43,7 @@ function defaultConfig(modelPath, config) { max_windows_per_payload: 128, inference_batch_size: 16, ...config, + model_path: modelPath, }; } diff --git a/crates/node/tests/pii_rampart_tests.mjs b/crates/node/tests/pii_rampart_tests.mjs index 60ae5d9a9..31df188d3 100644 --- a/crates/node/tests/pii_rampart_tests.mjs +++ b/crates/node/tests/pii_rampart_tests.mjs @@ -21,6 +21,7 @@ describe('pii_rampart plugin helpers', () => { const component = rampart.ComponentSpec(config); assert.equal(component.kind, rampart.RAMPART_PII_PLUGIN_KIND); assert.equal(component.enabled, true); + assert.equal(rampart.ComponentSpec(config, { enabled: false }).enabled, false); assert.deepEqual(rampart.validateConfig(config).diagnostics, []); }); @@ -46,6 +47,13 @@ describe('pii_rampart plugin helpers', () => { }), /requires target_paths or target_path_patterns/, ); + assert.equal( + rampart.defaultConfig('/models/rampart', { + model_path: '/unapproved/model', + target_paths: ['/message'], + }).model_path, + '/models/rampart', + ); }); it('is registered and validates malformed paths', () => { diff --git a/crates/pii-redaction/Cargo.toml b/crates/pii-redaction/Cargo.toml index b8fb07b77..88c600033 100644 --- a/crates/pii-redaction/Cargo.toml +++ b/crates/pii-redaction/Cargo.toml @@ -26,7 +26,7 @@ serde_json = "1" regex = "1" sha2 = "0.11" schemars = { version = "0.8", optional = true } -tokio = { version = "1", features = ["rt"], optional = true } +tokio = { version = "1", features = ["rt", "sync", "time"], optional = true } tract-onnx = { version = "0.23.4", optional = true } unicode_categories = { version = "0.1.1", optional = true } unicode-normalization = { version = "0.1.25", optional = true } diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index 3b4c0beae..b52b3dbca 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -267,6 +267,10 @@ target_path_patterns = [ accepts `*` as one complete path segment. At least one selector is required. When a supported codec is active, selectors address the normalized Relay request or response shape. +For OpenAI Chat responses with multiple choices, selectors under `message`, +`tool_calls`, `finish_reason`, or `api_specific` omit the complete observable +response because the normalized codec shape cannot safely project one choice +back onto every provider choice. ### Use a Language Binding @@ -285,9 +289,10 @@ Configuration helpers are available through these binding modules: Go and the raw C FFI remain experimental and source-first. Model loading and inference run in the shared Rust implementation for every binding. -Rampart admits one sanitizer operation before submitting work to Tokio's -blocking pool. Concurrent operations do not queue for model inference. -They fail closed immediately: tool observability payloads become the configured +Rampart runs at most two sanitizer operations concurrently and admits at most +eight operations per activation. Admitted operations wait asynchronously for +up to 250 ms before entering Tokio's blocking pool. A full admission queue or +expired wait fails closed: tool observability payloads become the configured replacement, LLM bodies are omitted, and mutable mark or generic scope fields are omitted. Tool and LLM scope metadata is omitted independently so an already-sanitized specialized payload remains available. These fallbacks do not diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index fc676d471..ce8edf2b9 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -60,8 +60,9 @@ pub(super) struct Detection { pub(super) struct RampartDetector { tokenizer: RampartTokenizer, - // Single-flight admission is enforced before this immutable plan enters - // Tokio's blocking pool. + // Each run creates invocation-local tract state. The sanitizer bounds how + // many callers may share this immutable plan before entering the blocking + // pool. plan: Arc, labels: Arc<[String]>, cls_id: i64, diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index 0111bf202..7e64ede69 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; use nemo_relay::api::event::{Event, EventSanitizeFields}; use nemo_relay::api::llm::LlmRequest; @@ -21,6 +21,7 @@ use nemo_relay::plugin::{PluginError, Result as PluginResult}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::{Map, Value as Json}; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::builtin::escape_json_pointer_segment; use crate::overlay::BuiltinCodecName; @@ -31,6 +32,12 @@ use super::model::{Detection, RampartDetector}; const MAX_TEXT_BYTES: usize = 16 * 1024; const MAX_TEXTS_PER_PAYLOAD: usize = 256; const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; +// Two model runs balance fan-out throughput against full-window tensor memory. +const MAX_CONCURRENT_INFERENCE: usize = 2; +// Bound admitted work and its wait so large payloads cannot build a long queue. +const MAX_ADMITTED_INFERENCE: usize = 8; +const MAX_ADMISSION_WAIT: Duration = Duration::from_millis(250); + pub(super) trait DetectionModel: Send + Sync { fn detect(&self, texts: &[&str]) -> PluginResult>; } @@ -50,7 +57,8 @@ pub(super) struct RampartSanitizer { excluded_labels: Arc>, replacement: Arc, legacy_surface: Option, - admission: Arc, + admission_capacity: Arc, + execution_admission: Arc, } #[derive(Clone)] @@ -87,13 +95,8 @@ enum EventField { } struct SanitizerPermit { - admission: Arc, -} - -impl Drop for SanitizerPermit { - fn drop(&mut self) { - self.admission.store(false, Ordering::Release); - } + _admission: OwnedSemaphorePermit, + _execution: OwnedSemaphorePermit, } impl RampartSanitizer { @@ -127,31 +130,52 @@ impl RampartSanitizer { excluded_labels: Arc::new(config.excluded_labels.into_iter().collect()), replacement: config.replacement.into(), legacy_surface, - admission: Arc::new(AtomicBool::new(false)), + admission_capacity: Arc::new(Semaphore::new(MAX_ADMITTED_INFERENCE)), + execution_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_INFERENCE)), }) } - fn try_admit(&self, surface: &'static str) -> Option { - if self - .admission - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_err() + async fn admit(&self, surface: &'static str) -> Option { + let admission = match Arc::clone(&self.admission_capacity).try_acquire_owned() { + Ok(permit) => permit, + Err(_) => { + self.log_admission_failure(surface, "queue_full"); + return None; + } + }; + let execution = match tokio::time::timeout( + MAX_ADMISSION_WAIT, + Arc::clone(&self.execution_admission).acquire_owned(), + ) + .await { - log::warn!( - target: "nemo_relay.plugin", - event = "rampart_pii_inference_failed", - plugin_kind = super::RAMPART_PII_PLUGIN_KIND, - reason = "contention", - surface; - "Rampart PII sanitization failed closed before blocking admission" - ); - return None; - } + Ok(Ok(permit)) => permit, + Ok(Err(_)) => { + self.log_admission_failure(surface, "closed"); + return None; + } + Err(_) => { + self.log_admission_failure(surface, "timeout"); + return None; + } + }; Some(SanitizerPermit { - admission: Arc::clone(&self.admission), + _admission: admission, + _execution: execution, }) } + fn log_admission_failure(&self, surface: &'static str, reason: &'static str) { + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + reason, + surface; + "Rampart PII sanitization failed closed during bounded admission" + ); + } + fn fail_closed_payload(&self) -> Json { Json::String(self.replacement.to_string()) } @@ -248,6 +272,29 @@ impl RampartSanitizer { } } + fn has_selected_string(&self, value: &Json) -> bool { + self.has_selected_string_at(value, &mut Vec::new()) + } + + fn has_selected_string_at(&self, value: &Json, path: &mut Vec) -> bool { + match value { + Json::String(_) => self.matches_path(path), + Json::Array(items) => items.iter().enumerate().any(|(index, item)| { + path.push(index.to_string()); + let selected = self.has_selected_string_at(item, path); + path.pop(); + selected + }), + Json::Object(fields) => fields.iter().any(|(key, value)| { + path.push(escape_json_pointer_segment(key)); + let selected = self.has_selected_string_at(value, path); + path.pop(); + selected + }), + _ => false, + } + } + fn replace_strings( &self, value: &mut Json, @@ -484,12 +531,14 @@ impl RampartSanitizer { pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeFn { Arc::new(move |_name, payload| { let backend = backend.clone(); - let Some(permit) = backend.try_admit("tool") else { - let payload = backend.fail_closed_payload(); + if !backend.has_selected_string(&payload) { return Box::pin(async move { Ok(payload) }); - }; + } let fallback = backend.fail_closed_payload(); Box::pin(async move { + let Some(permit) = backend.admit("tool").await else { + return Ok(fallback); + }; run_blocking("tool payload", permit, fallback, move || { backend.sanitize_json(payload) }) @@ -510,12 +559,14 @@ pub(super) fn event_sanitize_callback( if !event_has_candidate_fields(event.as_ref(), &fields) { return Box::pin(async move { Ok(fields) }); } - let Some(permit) = backend.try_admit("event") else { - let fields = fail_closed_event_fields(event.as_ref(), fields); + if !event_fields_have_selected_strings(&backend, event.as_ref(), &fields) { return Box::pin(async move { Ok(fields) }); - }; + } let fallback = fail_closed_event_fields(event.as_ref(), fields.clone()); Box::pin(async move { + let Some(permit) = backend.admit("event").await else { + return Ok(fallback); + }; run_blocking("event fields", permit, fallback, move || { let specialized_scope = is_specialized_scope(event.as_ref()); @@ -559,10 +610,10 @@ pub(super) fn event_sanitize_callback( pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSanitizeRequestFn { Arc::new(move |request, context| { let backend = backend.clone(); - let Some(permit) = backend.try_admit("llm_request") else { - return Box::pin(async move { Ok(None) }); - }; Box::pin(async move { + let Some(permit) = backend.admit("llm_request").await else { + return Ok(None); + }; run_blocking("LLM request", permit, None, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() @@ -598,10 +649,10 @@ pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSan pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSanitizeResponseFn { Arc::new(move |payload, context| { let backend = backend.clone(); - let Some(permit) = backend.try_admit("llm_response") else { - return Box::pin(async move { Ok(None) }); - }; Box::pin(async move { + let Some(permit) = backend.admit("llm_response").await else { + return Ok(None); + }; run_blocking("LLM response", permit, None, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() @@ -705,6 +756,32 @@ fn event_has_candidate_fields(event: &Event, fields: &EventSanitizeFields) -> bo } } +fn event_fields_have_selected_strings( + backend: &RampartSanitizer, + event: &Event, + fields: &EventSanitizeFields, +) -> bool { + if is_specialized_scope(event) { + return fields + .metadata + .as_ref() + .is_some_and(|metadata| backend.has_selected_string(metadata)); + } + fields + .data + .as_ref() + .is_some_and(|data| backend.has_selected_string(data)) + || fields + .category_profile + .as_ref() + .and_then(|profile| serde_json::to_value(profile).ok()) + .is_some_and(|profile| backend.has_selected_string(&profile)) + || fields + .metadata + .as_ref() + .is_some_and(|metadata| backend.has_selected_string(metadata)) +} + fn is_specialized_scope(event: &Event) -> bool { matches!(event, Event::Scope(_)) && event @@ -795,6 +872,21 @@ mod tests { } } + struct CountingBlockingDetector { + started: Arc, + release: Arc, + } + + impl DetectionModel for CountingBlockingDetector { + fn detect(&self, _texts: &[&str]) -> PluginResult> { + self.started.fetch_add(1, Ordering::Release); + while !self.release.load(Ordering::Acquire) { + std::thread::sleep(Duration::from_millis(1)); + } + Ok(Vec::new()) + } + } + fn sanitizer(detector: Arc, patterns: Vec<&str>) -> RampartSanitizer { RampartSanitizer::new( RampartPiiConfig { @@ -926,6 +1018,59 @@ mod tests { ); } + #[test] + fn bounded_fanout_does_not_block_the_runtime_thread() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .max_blocking_threads(MAX_CONCURRENT_INFERENCE) + .build() + .unwrap(); + runtime.block_on(async { + let started = Arc::new(AtomicUsize::new(0)); + let release = Arc::new(AtomicBool::new(false)); + let backend = sanitizer( + Arc::new(CountingBlockingDetector { + started: Arc::clone(&started), + release: Arc::clone(&release), + }), + vec!["/message"], + ); + let callback = tool_sanitize_callback(backend); + let mut tasks = Vec::new(); + for index in 0..MAX_ADMITTED_INFERENCE { + tasks.push(tokio::spawn(callback( + format!("tool-{index}"), + serde_json::json!({"message": "private"}), + ))); + } + tokio::time::timeout(Duration::from_secs(1), async { + while started.load(Ordering::Acquire) != MAX_CONCURRENT_INFERENCE { + tokio::task::yield_now().await; + } + }) + .await + .expect("the bounded model slots should start"); + + let heartbeat = tokio::spawn(async { + for _ in 0..4 { + tokio::time::sleep(Duration::from_millis(1)).await; + } + }); + heartbeat + .await + .expect("the runtime should remain responsive during fanout"); + + release.store(true, Ordering::Release); + for task in tasks { + assert_eq!( + task.await.unwrap().unwrap(), + serde_json::json!({"message": "private"}) + ); + } + assert_eq!(started.load(Ordering::Acquire), MAX_ADMITTED_INFERENCE); + }); + } + #[tokio::test(flavor = "current_thread")] async fn blocking_task_panics_fail_closed_for_every_surface() { use nemo_relay::api::event::{BaseEvent, MarkEvent}; @@ -985,10 +1130,10 @@ mod tests { } #[test] - fn contention_does_not_queue_behind_the_blocking_pool() { + fn bounded_admission_times_out_before_spawning_more_blocking_work() { let runtime = tokio::runtime::Builder::new_current_thread() .enable_time() - .max_blocking_threads(1) + .max_blocking_threads(MAX_CONCURRENT_INFERENCE) .build() .unwrap(); runtime.block_on(async { @@ -1003,13 +1148,17 @@ mod tests { }), vec!["/message"], ); + let execution = Arc::clone(&backend.execution_admission); let callback = tool_sanitize_callback(backend); - let active = tokio::spawn(callback( - "active".into(), - serde_json::json!({"message": "private"}), - )); + let mut active = Vec::new(); + for index in 0..MAX_CONCURRENT_INFERENCE { + active.push(tokio::spawn(callback( + format!("active-{index}"), + serde_json::json!({"message": "private"}), + ))); + } tokio::time::timeout(Duration::from_secs(1), async { - while !started.load(Ordering::Acquire) { + while !started.load(Ordering::Acquire) || execution.available_permits() != 0 { tokio::task::yield_now().await; } }) @@ -1017,22 +1166,24 @@ mod tests { .expect("blocking detector should start"); let contending = tokio::time::timeout( - Duration::from_millis(100), + MAX_ADMISSION_WAIT + Duration::from_millis(100), callback( "contending".into(), serde_json::json!({"message": "private", "metadata": "visible"}), ), ) .await - .expect("contending sanitizer should not enter the blocking pool") + .expect("contending sanitizer should respect the admission deadline") .unwrap(); assert_eq!(contending, Json::String("[REDACTED]".into())); release.store(true, Ordering::Release); - assert_eq!( - active.await.unwrap().unwrap(), - serde_json::json!({"message": "private"}) - ); + for task in active { + assert_eq!( + task.await.unwrap().unwrap(), + serde_json::json!({"message": "private"}) + ); + } }); } @@ -1049,7 +1200,7 @@ mod tests { }), vec!["/message"], ); - let admission = Arc::clone(&backend.admission); + let execution = Arc::clone(&backend.execution_admission); let callback = tool_sanitize_callback(backend); let active = tokio::spawn(callback( "active".into(), @@ -1062,22 +1213,21 @@ mod tests { }) .await .expect("blocking detector should start"); + assert_eq!(execution.available_permits(), MAX_CONCURRENT_INFERENCE - 1); active.abort(); assert!(active.await.unwrap_err().is_cancelled()); assert_eq!( - callback( - "contending".into(), - serde_json::json!({"message": "private"}), - ) - .await - .unwrap(), - Json::String("[REDACTED]".into()) + execution.available_permits(), + MAX_CONCURRENT_INFERENCE - 1, + "cancelling the async caller must not release an in-flight model slot" ); release.store(true, Ordering::Release); tokio::time::timeout(Duration::from_secs(1), async { - while !finished.load(Ordering::Acquire) || admission.load(Ordering::Acquire) { + while !finished.load(Ordering::Acquire) + || execution.available_permits() != MAX_CONCURRENT_INFERENCE + { tokio::task::yield_now().await; } }) @@ -1094,6 +1244,150 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn cancelled_queued_callback_releases_capacity_without_running() { + let started = Arc::new(AtomicUsize::new(0)); + let release = Arc::new(AtomicBool::new(false)); + let backend = sanitizer( + Arc::new(CountingBlockingDetector { + started: Arc::clone(&started), + release: Arc::clone(&release), + }), + vec!["/message"], + ); + let admission = Arc::clone(&backend.admission_capacity); + let execution = Arc::clone(&backend.execution_admission); + let callback = tool_sanitize_callback(backend); + let mut active = Vec::new(); + for index in 0..MAX_CONCURRENT_INFERENCE { + active.push(tokio::spawn(callback( + format!("active-{index}"), + serde_json::json!({"message": "private"}), + ))); + } + tokio::time::timeout(Duration::from_secs(1), async { + while started.load(Ordering::Acquire) != MAX_CONCURRENT_INFERENCE + || execution.available_permits() != 0 + { + tokio::task::yield_now().await; + } + }) + .await + .expect("both model slots should start"); + + let queued = tokio::spawn(callback( + "queued".into(), + serde_json::json!({"message": "private"}), + )); + tokio::time::timeout(Duration::from_millis(50), async { + while admission.available_permits() != MAX_ADMITTED_INFERENCE - 3 { + tokio::task::yield_now().await; + } + }) + .await + .expect("the queued callback should reserve bounded capacity"); + queued.abort(); + assert!(queued.await.unwrap_err().is_cancelled()); + assert_eq!(admission.available_permits(), MAX_ADMITTED_INFERENCE - 2); + assert_eq!(started.load(Ordering::Acquire), MAX_CONCURRENT_INFERENCE); + + release.store(true, Ordering::Release); + for task in active { + task.await.unwrap().unwrap(); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn full_admission_queue_fails_closed_without_running() { + let calls = Arc::new(AtomicUsize::new(0)); + let backend = sanitizer( + Arc::new(CountingDetector(Arc::clone(&calls))), + vec!["/message"], + ); + let mut permits = Vec::new(); + for _ in 0..MAX_ADMITTED_INFERENCE { + permits.push( + Arc::clone(&backend.admission_capacity) + .try_acquire_owned() + .unwrap(), + ); + } + let output = tool_sanitize_callback(backend)( + "queue-full".into(), + serde_json::json!({"message": "private"}), + ) + .await + .unwrap(); + assert_eq!(output, Json::String("[REDACTED]".into())); + assert_eq!(calls.load(Ordering::Acquire), 0); + drop(permits); + } + + #[tokio::test(flavor = "current_thread")] + async fn unselected_tool_payload_bypasses_full_admission_queue() { + let calls = Arc::new(AtomicUsize::new(0)); + let backend = sanitizer( + Arc::new(CountingDetector(Arc::clone(&calls))), + vec!["/message"], + ); + let mut permits = Vec::new(); + for _ in 0..MAX_ADMITTED_INFERENCE { + permits.push( + Arc::clone(&backend.admission_capacity) + .try_acquire_owned() + .unwrap(), + ); + } + let payload = serde_json::json!({"trace_id": "visible"}); + let output = tool_sanitize_callback(backend)("unselected".into(), payload.clone()) + .await + .unwrap(); + assert_eq!(output, payload); + assert_eq!(calls.load(Ordering::Acquire), 0); + drop(permits); + } + + #[tokio::test(flavor = "current_thread")] + async fn unselected_specialized_metadata_bypasses_full_admission_queue() { + use nemo_relay::api::event::{ + BaseEvent, CategoryProfile, EventCategory, ScopeCategory, ScopeEvent, + }; + + let calls = Arc::new(AtomicUsize::new(0)); + let backend = sanitizer( + Arc::new(CountingDetector(Arc::clone(&calls))), + vec!["/message"], + ); + let mut permits = Vec::new(); + for _ in 0..MAX_ADMITTED_INFERENCE { + permits.push( + Arc::clone(&backend.admission_capacity) + .try_acquire_owned() + .unwrap(), + ); + } + let event = Arc::new(Event::Scope(ScopeEvent::new( + BaseEvent::builder() + .name("tool") + .metadata(serde_json::json!({"trace_id": "visible"})) + .build(), + ScopeCategory::Start, + Default::default(), + EventCategory::tool(), + Some(CategoryProfile::default()), + ))); + let fields = event.sanitize_fields(); + let output = event_sanitize_callback(backend, Some((false, true)))( + Arc::clone(&event), + fields.clone(), + ) + .await + .unwrap(); + assert_eq!(output, fields); + assert_eq!(calls.load(Ordering::Acquire), 0); + drop(permits); + } + #[test] fn empty_specialized_scope_does_not_require_admission() { use nemo_relay::api::event::{ diff --git a/crates/pii-redaction/src/rampart/tokenizer.rs b/crates/pii-redaction/src/rampart/tokenizer.rs index 24db06a83..0c6a3e0f7 100644 --- a/crates/pii-redaction/src/rampart/tokenizer.rs +++ b/crates/pii-redaction/src/rampart/tokenizer.rs @@ -185,12 +185,10 @@ fn normalize(text: &str, base_offset: usize) -> Vec { for (relative_start, original) in text.char_indices() { let original_start = base_offset + relative_start; let original_end = original_start + original.len_utf8(); - // Rampart's model card recommends splitting hyphenated identifiers. - // Replacing one ASCII byte preserves the original byte offsets. if original == '\0' || original == '\u{fffd}' || is_control(original) { continue; } - let cleaned = if original == '-' || is_whitespace(original) { + let cleaned = if is_whitespace(original) { ' ' } else { original @@ -288,6 +286,7 @@ mod tests { "##ph", "野", "alice", + "-", "rivera", "alicerivera", ]; @@ -313,10 +312,10 @@ mod tests { } #[test] - fn splits_hyphens_without_shifting_offsets() { + fn preserves_hyphen_token_and_original_offsets() { let encoded = tokenizer().encode("Alice-Rivera").unwrap(); - assert_eq!(encoded.ids, [10, 11]); - assert_eq!(encoded.offsets, [(0, 5), (6, 12)]); + assert_eq!(encoded.ids, [10, 11, 12]); + assert_eq!(encoded.offsets, [(0, 5), (5, 6), (6, 12)]); } #[test] @@ -336,7 +335,7 @@ mod tests { #[test] fn removes_controls_and_combining_marks_without_shifting_original_offsets() { let encoded = tokenizer().encode("Alice\0Cafe\u{301} Rivera").unwrap(); - assert_eq!(encoded.ids, [1, 11]); + assert_eq!(encoded.ids, [1, 12]); assert_eq!(encoded.offsets, [(0, 10), (13, 19)]); } @@ -345,7 +344,7 @@ mod tests { for control in ['\u{000b}', '\u{000c}', '\u{0085}'] { let text = format!("Alice{control}Rivera"); let encoded = tokenizer().encode(&text).unwrap(); - assert_eq!(encoded.ids, [12], "control U+{:04X}", control as u32); + assert_eq!(encoded.ids, [13], "control U+{:04X}", control as u32); assert_eq!( encoded.offsets, [(0, text.len())], @@ -363,7 +362,7 @@ mod tests { let encoded = tokenizer().encode(&text).unwrap(); assert_eq!( encoded.ids, - [10, 11], + [10, 12], "whitespace U+{:04X}", whitespace as u32 ); From 9639f29da1ac60e72bf64d9d553f9a2f4e58e621 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Fri, 31 Jul 2026 16:02:49 -0700 Subject: [PATCH 76/83] fix(pii): isolate Rampart inference workers Signed-off-by: Alex Fournier --- Cargo.lock | 1 + crates/pii-redaction/Cargo.toml | 3 +- crates/pii-redaction/README.md | 8 +- crates/pii-redaction/src/rampart/sanitizer.rs | 175 ++++++++++++++---- 4 files changed, 148 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34a253198..f95076ea5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2072,6 +2072,7 @@ dependencies = [ "log", "nemo-relay", "opentelemetry_sdk", + "rayon", "regex", "schemars", "serde", diff --git a/crates/pii-redaction/Cargo.toml b/crates/pii-redaction/Cargo.toml index 88c600033..069145e97 100644 --- a/crates/pii-redaction/Cargo.toml +++ b/crates/pii-redaction/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [features] default = [] -rampart = ["dep:tokio", "dep:tract-onnx", "dep:unicode_categories", "dep:unicode-normalization"] +rampart = ["dep:rayon", "dep:tokio", "dep:tract-onnx", "dep:unicode_categories", "dep:unicode-normalization"] schema = ["dep:schemars", "nemo-relay/schema"] [dependencies] @@ -24,6 +24,7 @@ log = { version = "0.4", features = ["kv"] } serde = { version = "1", features = ["derive"] } serde_json = "1" regex = "1" +rayon = { version = "1.12", optional = true } sha2 = "0.11" schemars = { version = "0.8", optional = true } tokio = { version = "1", features = ["rt", "sync", "time"], optional = true } diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index b52b3dbca..b610c0d7b 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -289,9 +289,11 @@ Configuration helpers are available through these binding modules: Go and the raw C FFI remain experimental and source-first. Model loading and inference run in the shared Rust implementation for every binding. -Rampart runs at most two sanitizer operations concurrently and admits at most -eight operations per activation. Admitted operations wait asynchronously for -up to 250 ms before entering Tokio's blocking pool. A full admission queue or +Rampart uses a dedicated CPU executor with up to three workers per activation, +limited further by the host's available parallelism. It admits at most 16 +sanitizer operations, and admitted operations wait asynchronously for an +inference worker for up to 500 ms. This keeps model inference off both Tokio +executor threads and Tokio's shared blocking pool. A full admission queue or expired wait fails closed: tool observability payloads become the configured replacement, LLM bodies are omitted, and mutable mark or generic scope fields are omitted. Tool and LLM scope metadata is omitted independently so an diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index 7e64ede69..e54fd303d 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::collections::HashSet; +use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Arc; use std::time::Duration; @@ -18,6 +19,7 @@ use nemo_relay::codec::resolve::{ use nemo_relay::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_relay::error::Result as FlowResult; use nemo_relay::plugin::{PluginError, Result as PluginResult}; +use rayon::{ThreadPool, ThreadPoolBuilder}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::{Map, Value as Json}; @@ -32,11 +34,18 @@ use super::model::{Detection, RampartDetector}; const MAX_TEXT_BYTES: usize = 16 * 1024; const MAX_TEXTS_PER_PAYLOAD: usize = 256; const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; -// Two model runs balance fan-out throughput against full-window tensor memory. -const MAX_CONCURRENT_INFERENCE: usize = 2; +// Cap CPU workers while respecting smaller hosts and container CPU quotas. +const MAX_CONCURRENT_INFERENCE: usize = 3; // Bound admitted work and its wait so large payloads cannot build a long queue. -const MAX_ADMITTED_INFERENCE: usize = 8; -const MAX_ADMISSION_WAIT: Duration = Duration::from_millis(250); +const MAX_ADMITTED_INFERENCE: usize = 16; +const MAX_ADMISSION_WAIT: Duration = Duration::from_millis(500); + +fn inference_worker_count() -> usize { + std::thread::available_parallelism() + .map(std::num::NonZeroUsize::get) + .unwrap_or(1) + .min(MAX_CONCURRENT_INFERENCE) +} pub(super) trait DetectionModel: Send + Sync { fn detect(&self, texts: &[&str]) -> PluginResult>; @@ -59,6 +68,7 @@ pub(super) struct RampartSanitizer { legacy_surface: Option, admission_capacity: Arc, execution_admission: Arc, + executor: Arc, } #[derive(Clone)] @@ -97,6 +107,30 @@ enum EventField { struct SanitizerPermit { _admission: OwnedSemaphorePermit, _execution: OwnedSemaphorePermit, + executor: Arc, +} + +struct SanitizerExecutor { + pool: ThreadPool, +} + +impl SanitizerExecutor { + fn new(worker_count: usize) -> PluginResult { + let pool = ThreadPoolBuilder::new() + .num_threads(worker_count) + .thread_name(|worker| format!("nemo-relay-rampart-{worker}")) + .build() + .map_err(|error| { + PluginError::Internal(format!( + "failed to start Rampart inference workers: {error}" + )) + })?; + Ok(Self { pool }) + } + + fn submit(&self, job: impl FnOnce() + Send + 'static) { + self.pool.spawn_fifo(job); + } } impl RampartSanitizer { @@ -110,6 +144,8 @@ impl RampartSanitizer { })?), None => None, }; + let worker_count = inference_worker_count(); + let executor = Arc::new(SanitizerExecutor::new(worker_count)?); Ok(Self { detector, target_paths: Arc::new( @@ -131,7 +167,8 @@ impl RampartSanitizer { replacement: config.replacement.into(), legacy_surface, admission_capacity: Arc::new(Semaphore::new(MAX_ADMITTED_INFERENCE)), - execution_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_INFERENCE)), + execution_admission: Arc::new(Semaphore::new(worker_count)), + executor, }) } @@ -162,6 +199,7 @@ impl RampartSanitizer { Some(SanitizerPermit { _admission: admission, _execution: execution, + executor: Arc::clone(&self.executor), }) } @@ -539,7 +577,7 @@ pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeF let Some(permit) = backend.admit("tool").await else { return Ok(fallback); }; - run_blocking("tool payload", permit, fallback, move || { + run_inference("tool payload", permit, fallback, move || { backend.sanitize_json(payload) }) .await @@ -567,7 +605,7 @@ pub(super) fn event_sanitize_callback( let Some(permit) = backend.admit("event").await else { return Ok(fallback); }; - run_blocking("event fields", permit, fallback, move || { + run_inference("event fields", permit, fallback, move || { let specialized_scope = is_specialized_scope(event.as_ref()); let mut selected = Vec::with_capacity(3); @@ -614,7 +652,7 @@ pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSan let Some(permit) = backend.admit("llm_request").await else { return Ok(None); }; - run_blocking("LLM request", permit, None, move || { + run_inference("LLM request", permit, None, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { @@ -653,7 +691,7 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa let Some(permit) = backend.admit("llm_response").await else { return Ok(None); }; - run_blocking("LLM response", permit, None, move || { + run_inference("LLM response", permit, None, move || { if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { @@ -695,7 +733,7 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa }) } -async fn run_blocking( +async fn run_inference( target: &'static str, permit: SanitizerPermit, fallback: T, @@ -704,22 +742,34 @@ async fn run_blocking( where T: Send + 'static, { - match tokio::task::spawn_blocking(move || { + let executor = Arc::clone(&permit.executor); + let (sender, receiver) = tokio::sync::oneshot::channel(); + executor.submit(move || { let _permit = permit; - operation() - }) - .await - { - Ok(value) => Ok(value), - Err(error) => { + let _ = sender.send(catch_unwind(AssertUnwindSafe(operation))); + }); + match receiver.await { + Ok(Ok(value)) => Ok(value), + Ok(Err(_)) => { log::error!( target: "nemo_relay.plugin", event = "rampart_pii_inference_failed", plugin_kind = super::RAMPART_PII_PLUGIN_KIND, - reason = "blocking_task", + reason = "dedicated_executor_panic", target, - panicked = error.is_panic(); - "Rampart PII blocking sanitization failed closed: {error}" + panicked = true; + "Rampart PII inference worker panicked and failed closed" + ); + Ok(fallback) + } + Err(error) => { + log::error!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + reason = "dedicated_executor_result", + target; + "Rampart PII inference worker lost a result and failed closed: {error}" ); Ok(fallback) } @@ -1020,9 +1070,10 @@ mod tests { #[test] fn bounded_fanout_does_not_block_the_runtime_thread() { + let worker_count = inference_worker_count(); let runtime = tokio::runtime::Builder::new_current_thread() .enable_time() - .max_blocking_threads(MAX_CONCURRENT_INFERENCE) + .max_blocking_threads(1) .build() .unwrap(); runtime.block_on(async { @@ -1044,7 +1095,7 @@ mod tests { ))); } tokio::time::timeout(Duration::from_secs(1), async { - while started.load(Ordering::Acquire) != MAX_CONCURRENT_INFERENCE { + while started.load(Ordering::Acquire) != worker_count { tokio::task::yield_now().await; } }) @@ -1071,8 +1122,57 @@ mod tests { }); } + #[test] + fn dedicated_executor_ignores_saturated_tokio_blocking_pool() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .max_blocking_threads(1) + .build() + .unwrap(); + runtime.block_on(async { + let blocker_started = Arc::new(AtomicBool::new(false)); + let blocker_release = Arc::new(AtomicBool::new(false)); + let started = Arc::clone(&blocker_started); + let release = Arc::clone(&blocker_release); + let blocker = tokio::task::spawn_blocking(move || { + started.store(true, Ordering::Release); + while !release.load(Ordering::Acquire) { + std::thread::yield_now(); + } + }); + tokio::time::timeout(Duration::from_secs(1), async { + while !blocker_started.load(Ordering::Acquire) { + tokio::task::yield_now().await; + } + }) + .await + .expect("the Tokio blocking-pool fixture should start"); + + let calls = Arc::new(AtomicUsize::new(0)); + let callback = tool_sanitize_callback(sanitizer( + Arc::new(CountingDetector(Arc::clone(&calls))), + vec!["/message"], + )); + let output = tokio::time::timeout( + Duration::from_millis(250), + callback( + "dedicated-executor".into(), + serde_json::json!({"message": "private"}), + ), + ) + .await + .expect("Rampart must not queue behind Tokio's blocking pool") + .unwrap(); + assert_eq!(output, serde_json::json!({"message": "private"})); + assert_eq!(calls.load(Ordering::Acquire), 1); + + blocker_release.store(true, Ordering::Release); + blocker.await.unwrap(); + }); + } + #[tokio::test(flavor = "current_thread")] - async fn blocking_task_panics_fail_closed_for_every_surface() { + async fn inference_worker_panics_fail_closed_for_every_surface() { use nemo_relay::api::event::{BaseEvent, MarkEvent}; use nemo_relay::api::runtime::{LlmSanitizeRequestContext, LlmSanitizeResponseContext}; @@ -1131,9 +1231,10 @@ mod tests { #[test] fn bounded_admission_times_out_before_spawning_more_blocking_work() { + let worker_count = inference_worker_count(); let runtime = tokio::runtime::Builder::new_current_thread() .enable_time() - .max_blocking_threads(MAX_CONCURRENT_INFERENCE) + .max_blocking_threads(1) .build() .unwrap(); runtime.block_on(async { @@ -1151,7 +1252,7 @@ mod tests { let execution = Arc::clone(&backend.execution_admission); let callback = tool_sanitize_callback(backend); let mut active = Vec::new(); - for index in 0..MAX_CONCURRENT_INFERENCE { + for index in 0..worker_count { active.push(tokio::spawn(callback( format!("active-{index}"), serde_json::json!({"message": "private"}), @@ -1201,6 +1302,7 @@ mod tests { vec!["/message"], ); let execution = Arc::clone(&backend.execution_admission); + let worker_count = execution.available_permits(); let callback = tool_sanitize_callback(backend); let active = tokio::spawn(callback( "active".into(), @@ -1213,26 +1315,25 @@ mod tests { }) .await .expect("blocking detector should start"); - assert_eq!(execution.available_permits(), MAX_CONCURRENT_INFERENCE - 1); + assert_eq!(execution.available_permits(), worker_count - 1); active.abort(); assert!(active.await.unwrap_err().is_cancelled()); assert_eq!( execution.available_permits(), - MAX_CONCURRENT_INFERENCE - 1, + worker_count - 1, "cancelling the async caller must not release an in-flight model slot" ); release.store(true, Ordering::Release); tokio::time::timeout(Duration::from_secs(1), async { - while !finished.load(Ordering::Acquire) - || execution.available_permits() != MAX_CONCURRENT_INFERENCE + while !finished.load(Ordering::Acquire) || execution.available_permits() != worker_count { tokio::task::yield_now().await; } }) .await - .expect("detached blocking work should finish"); + .expect("detached inference work should finish"); assert_eq!( callback( "recovered".into(), @@ -1257,16 +1358,17 @@ mod tests { ); let admission = Arc::clone(&backend.admission_capacity); let execution = Arc::clone(&backend.execution_admission); + let worker_count = execution.available_permits(); let callback = tool_sanitize_callback(backend); let mut active = Vec::new(); - for index in 0..MAX_CONCURRENT_INFERENCE { + for index in 0..worker_count { active.push(tokio::spawn(callback( format!("active-{index}"), serde_json::json!({"message": "private"}), ))); } tokio::time::timeout(Duration::from_secs(1), async { - while started.load(Ordering::Acquire) != MAX_CONCURRENT_INFERENCE + while started.load(Ordering::Acquire) != worker_count || execution.available_permits() != 0 { tokio::task::yield_now().await; @@ -1280,7 +1382,7 @@ mod tests { serde_json::json!({"message": "private"}), )); tokio::time::timeout(Duration::from_millis(50), async { - while admission.available_permits() != MAX_ADMITTED_INFERENCE - 3 { + while admission.available_permits() != MAX_ADMITTED_INFERENCE - worker_count - 1 { tokio::task::yield_now().await; } }) @@ -1288,8 +1390,11 @@ mod tests { .expect("the queued callback should reserve bounded capacity"); queued.abort(); assert!(queued.await.unwrap_err().is_cancelled()); - assert_eq!(admission.available_permits(), MAX_ADMITTED_INFERENCE - 2); - assert_eq!(started.load(Ordering::Acquire), MAX_CONCURRENT_INFERENCE); + assert_eq!( + admission.available_permits(), + MAX_ADMITTED_INFERENCE - worker_count + ); + assert_eq!(started.load(Ordering::Acquire), worker_count); release.store(true, Ordering::Release); for task in active { From 75c550706154fb1d0c2c1dd415f2ca0122eee807 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 3 Aug 2026 10:53:31 -0700 Subject: [PATCH 77/83] fix(pii): address Rampart review feedback Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/mod.rs | 28 ++++++++++++------- go/nemo_relay/pii_rampart.go | 27 +++++++++++++++--- go/nemo_relay/pii_rampart/pii_rampart.go | 8 +++++- go/nemo_relay/pii_rampart/pii_rampart_test.go | 6 ++++ go/nemo_relay/pii_rampart_test.go | 6 ++++ 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs index 4c317eb71..28fa83456 100644 --- a/crates/pii-redaction/src/rampart/mod.rs +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -502,21 +502,19 @@ fn config_value_violations(config: &RampartPiiConfig) -> Vec { ), )); } - if config - .target_paths - .iter() - .any(|path| path.len() > MAX_TARGET_PATH_BYTES || !is_valid_json_pointer(path)) - { + if config.target_paths.iter().any(|path| { + path.is_empty() || path.len() > MAX_TARGET_PATH_BYTES || !is_valid_json_pointer(path) + }) { violations.push(ConfigViolation::new( "target_paths", "target_paths entries must be bounded valid JSON pointers", )); } - if config - .target_path_patterns - .iter() - .any(|path| path.len() > MAX_TARGET_PATH_BYTES || !is_valid_json_pointer_pattern(path)) - { + if config.target_path_patterns.iter().any(|path| { + path.is_empty() + || path.len() > MAX_TARGET_PATH_BYTES + || !is_valid_json_pointer_pattern(path) + }) { violations.push(ConfigViolation::new( "target_path_patterns", "target_path_patterns entries must be bounded JSON pointers with only complete '*' segments", @@ -713,6 +711,16 @@ mod tests { serde_json::json!(["messages/0/content"]), "target_paths entries", ), + ( + "target_paths", + serde_json::json!([""]), + "target_paths entries", + ), + ( + "target_path_patterns", + serde_json::json!([""]), + "target_path_patterns entries", + ), ("min_score", serde_json::json!(1.1), "min_score must"), ]; diff --git a/go/nemo_relay/pii_rampart.go b/go/nemo_relay/pii_rampart.go index 20580d58c..d10004aa5 100644 --- a/go/nemo_relay/pii_rampart.go +++ b/go/nemo_relay/pii_rampart.go @@ -33,6 +33,12 @@ type RampartPiiConfig struct { Policy *ConfigPolicy `json:"policy,omitempty"` } +// RampartPiiComponentSpec wraps one Rampart PII config as a top-level plugin component. +type RampartPiiComponentSpec struct { + Enabled bool `json:"enabled,omitempty"` + Config RampartPiiConfig `json:"config"` +} + // NewRampartPiiConfig returns Rampart PII settings with runtime defaults. func NewRampartPiiConfig(modelPath string) RampartPiiConfig { return RampartPiiConfig{ @@ -54,15 +60,28 @@ func NewRampartPiiConfig(modelPath string) RampartPiiConfig { } } -// RampartPiiComponent converts config into the shared plugin component. -func RampartPiiComponent(config RampartPiiConfig) PluginComponentSpec { +// NewRampartPiiComponentSpec wraps Rampart PII config as an enabled component. +func NewRampartPiiComponentSpec(config RampartPiiConfig) RampartPiiComponentSpec { + return RampartPiiComponentSpec{ + Enabled: true, + Config: config, + } +} + +// PluginComponent converts the Rampart PII wrapper into the shared plugin shape. +func (spec RampartPiiComponentSpec) PluginComponent() PluginComponentSpec { return PluginComponentSpec{ Kind: RampartPiiPluginKind, - Enabled: true, - Config: mustConfigMap(config), + Enabled: spec.Enabled, + Config: mustConfigMap(spec.Config), } } +// RampartPiiComponent converts config into the shared plugin component. +func RampartPiiComponent(config RampartPiiConfig) PluginComponentSpec { + return NewRampartPiiComponentSpec(config).PluginComponent() +} + // ValidateRampartPiiConfig validates config without loading model files. func ValidateRampartPiiConfig(config RampartPiiConfig) (ConfigReport, error) { return ValidatePluginConfig(PluginConfig{ diff --git a/go/nemo_relay/pii_rampart/pii_rampart.go b/go/nemo_relay/pii_rampart/pii_rampart.go index 24920fc93..61e8eeaf6 100644 --- a/go/nemo_relay/pii_rampart/pii_rampart.go +++ b/go/nemo_relay/pii_rampart/pii_rampart.go @@ -8,6 +8,7 @@ import nemo_relay "github.com/NVIDIA/NeMo-Relay/go/nemo_relay" type Config = nemo_relay.RampartPiiConfig type ConfigPolicy = nemo_relay.ConfigPolicy type ConfigReport = nemo_relay.ConfigReport +type ComponentSpec = nemo_relay.RampartPiiComponentSpec // PluginKind is the Rampart PII component kind. const PluginKind = nemo_relay.RampartPiiPluginKind @@ -23,9 +24,14 @@ func NewConfig(modelPath string) Config { return nemo_relay.NewRampartPiiConfig(modelPath) } +// NewComponentSpec wraps Rampart PII config as an enabled component. +func NewComponentSpec(config Config) ComponentSpec { + return nemo_relay.NewRampartPiiComponentSpec(config) +} + // Component converts config into the shared plugin component. func Component(config Config) nemo_relay.PluginComponentSpec { - return nemo_relay.RampartPiiComponent(config) + return NewComponentSpec(config).PluginComponent() } // ValidateConfig validates config without loading model files. diff --git a/go/nemo_relay/pii_rampart/pii_rampart_test.go b/go/nemo_relay/pii_rampart/pii_rampart_test.go index 5d342cb0f..371a8e6a4 100644 --- a/go/nemo_relay/pii_rampart/pii_rampart_test.go +++ b/go/nemo_relay/pii_rampart/pii_rampart_test.go @@ -19,4 +19,10 @@ func TestConfigAndComponentHelpers(t *testing.T) { ModelRevision != "b1993e4e68b082835b80ffc65acc03325ea2e501" { t.Fatalf("unexpected Rampart model identity: %s@%s", ModelID, ModelRevision) } + + disabled := NewComponentSpec(config) + disabled.Enabled = false + if disabled.PluginComponent().Enabled { + t.Fatal("disabled Rampart PII component was enabled") + } } diff --git a/go/nemo_relay/pii_rampart_test.go b/go/nemo_relay/pii_rampart_test.go index 07012ec13..c5d2be44c 100644 --- a/go/nemo_relay/pii_rampart_test.go +++ b/go/nemo_relay/pii_rampart_test.go @@ -22,6 +22,12 @@ func TestRampartPiiConfigHelpers(t *testing.T) { RampartModelRevision != "b1993e4e68b082835b80ffc65acc03325ea2e501" { t.Fatalf("unexpected Rampart model identity: %s@%s", RampartModelID, RampartModelRevision) } + + disabled := NewRampartPiiComponentSpec(config) + disabled.Enabled = false + if disabled.PluginComponent().Enabled { + t.Fatal("disabled Rampart PII component was enabled") + } } func TestRampartPiiConfigPreservesExplicitZeroValues(t *testing.T) { From 121f5639077379e2747948ea76cd0b9684bfc637 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 3 Aug 2026 10:54:35 -0700 Subject: [PATCH 78/83] docs(pii): clarify Rampart selector setup Signed-off-by: Alex Fournier --- go/nemo_relay/pii_rampart.go | 1 + go/nemo_relay/pii_rampart/pii_rampart.go | 1 + 2 files changed, 2 insertions(+) diff --git a/go/nemo_relay/pii_rampart.go b/go/nemo_relay/pii_rampart.go index d10004aa5..e1c476158 100644 --- a/go/nemo_relay/pii_rampart.go +++ b/go/nemo_relay/pii_rampart.go @@ -40,6 +40,7 @@ type RampartPiiComponentSpec struct { } // NewRampartPiiConfig returns Rampart PII settings with runtime defaults. +// Set TargetPaths or TargetPathPatterns before validation or activation. func NewRampartPiiConfig(modelPath string) RampartPiiConfig { return RampartPiiConfig{ Version: 1, diff --git a/go/nemo_relay/pii_rampart/pii_rampart.go b/go/nemo_relay/pii_rampart/pii_rampart.go index 61e8eeaf6..cbb50e273 100644 --- a/go/nemo_relay/pii_rampart/pii_rampart.go +++ b/go/nemo_relay/pii_rampart/pii_rampart.go @@ -20,6 +20,7 @@ const ModelID = nemo_relay.RampartModelID const ModelRevision = nemo_relay.RampartModelRevision // NewConfig returns Rampart PII settings with runtime defaults. +// Set TargetPaths or TargetPathPatterns before validation or activation. func NewConfig(modelPath string) Config { return nemo_relay.NewRampartPiiConfig(modelPath) } From f73f8500ec4dd8e6386a7e10239eb2337bde733e Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 3 Aug 2026 12:38:33 -0700 Subject: [PATCH 79/83] fix(pii): bound Rampart payloads atomically Signed-off-by: Alex Fournier --- crates/node/pii_rampart.js | 2 +- crates/node/tests/pii_rampart_tests.mjs | 1 + crates/pii-redaction/README.md | 11 +- crates/pii-redaction/src/rampart/mod.rs | 29 +- crates/pii-redaction/src/rampart/model.rs | 39 +- crates/pii-redaction/src/rampart/sanitizer.rs | 453 ++++++++++++------ go/nemo_relay/pii_rampart.go | 2 +- go/nemo_relay/pii_rampart_test.go | 3 + python/nemo_relay/pii_rampart.py | 2 +- python/tests/test_pii_rampart_plugin.py | 1 + 10 files changed, 381 insertions(+), 162 deletions(-) diff --git a/crates/node/pii_rampart.js b/crates/node/pii_rampart.js index cbf423742..2cbaed61c 100644 --- a/crates/node/pii_rampart.js +++ b/crates/node/pii_rampart.js @@ -40,7 +40,7 @@ function defaultConfig(modelPath, config) { min_score: 0.4, excluded_labels: [], replacement: '[REDACTED]', - max_windows_per_payload: 128, + max_windows_per_payload: 4, inference_batch_size: 16, ...config, model_path: modelPath, diff --git a/crates/node/tests/pii_rampart_tests.mjs b/crates/node/tests/pii_rampart_tests.mjs index 31df188d3..d1c3f2782 100644 --- a/crates/node/tests/pii_rampart_tests.mjs +++ b/crates/node/tests/pii_rampart_tests.mjs @@ -15,6 +15,7 @@ describe('pii_rampart plugin helpers', () => { target_path_patterns: ['/messages/*/content'], }); assert.equal(config.model_path, '/models/rampart'); + assert.equal(config.max_windows_per_payload, 4); assert.equal(config.inference_batch_size, 16); assert.equal(rampart.RAMPART_MODEL_ID, 'nationaldesignstudio/rampart'); assert.equal(rampart.RAMPART_MODEL_REVISION, 'b1993e4e68b082835b80ffc65acc03325ea2e501'); diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index b610c0d7b..bae4bed04 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -298,7 +298,16 @@ expired wait fails closed: tool observability payloads become the configured replacement, LLM bodies are omitted, and mutable mark or generic scope fields are omitted. Tool and LLM scope metadata is omitted independently so an already-sanitized specialized payload remains available. These fallbacks do not -change the arguments or return values seen by the underlying tool or model. +pass selected content through unsanitized. + +The default compute budget is four overlapping 512-token windows across all +selected strings in one payload. `max_windows_per_payload` can raise that budget +to at most 16. Relay also caps one payload at 256 selected strings and 256 KiB of +selected UTF-8 text to bound preprocessing memory. Exceeding any of these limits +logs `reason=payload_limit` and applies the same whole-surface fail-closed +behavior instead of partially sanitizing the payload. Sanitization only changes +emitted observability; it does not change arguments or return values seen by the +underlying tool or model. ## Documentation diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs index 28fa83456..2eac3f3e4 100644 --- a/crates/pii-redaction/src/rampart/mod.rs +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -42,6 +42,7 @@ const MAX_TARGET_PATH_BYTES: usize = 1024; const MAX_EXCLUDED_LABELS: usize = 128; const MAX_LABEL_BYTES: usize = 128; const MAX_REPLACEMENT_BYTES: usize = 1024; +const MAX_WINDOWS_PER_PAYLOAD: usize = 16; /// One configured Rampart PII component. #[derive(Debug, Clone)] @@ -547,10 +548,10 @@ fn config_value_violations(config: &RampartPiiConfig) -> Vec { format!("replacement must not exceed {MAX_REPLACEMENT_BYTES} UTF-8 bytes"), )); } - if !(1..=512).contains(&config.max_windows_per_payload) { + if !(1..=MAX_WINDOWS_PER_PAYLOAD).contains(&config.max_windows_per_payload) { violations.push(ConfigViolation::new( "max_windows_per_payload", - "max_windows_per_payload must be between 1 and 512", + format!("max_windows_per_payload must be between 1 and {MAX_WINDOWS_PER_PAYLOAD}"), )); } if !(1..=64).contains(&config.inference_batch_size) { @@ -644,7 +645,7 @@ fn default_replacement() -> String { } fn default_max_windows_per_payload() -> usize { - 128 + 4 } fn default_inference_batch_size() -> usize { @@ -703,6 +704,28 @@ mod tests { ); } + #[test] + fn bounds_the_configurable_window_budget() { + assert_eq!(RampartPiiConfig::default().max_windows_per_payload, 4); + + let mut config = valid_config(); + config.insert( + "max_windows_per_payload".into(), + Json::from(MAX_WINDOWS_PER_PAYLOAD), + ); + assert!(validate_rampart_pii_config(&config, None).is_empty()); + + config.insert( + "max_windows_per_payload".into(), + Json::from(MAX_WINDOWS_PER_PAYLOAD + 1), + ); + assert!( + validate_rampart_pii_config(&config, None) + .iter() + .any(|item| item.field.as_deref() == Some("max_windows_per_payload")) + ); + } + #[tokio::test] async fn registration_enforces_safety_invariants_when_policy_warns() { let cases = [ diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index ce8edf2b9..93468edf8 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -58,6 +58,26 @@ pub(super) struct Detection { pub(super) score: f64, } +pub(super) enum DetectionError { + PayloadLimit, + Model(PluginError), +} + +impl DetectionError { + fn into_plugin_error(self) -> PluginError { + match self { + Self::PayloadLimit => inference_error("Rampart warmup exceeded its payload limit"), + Self::Model(error) => error, + } + } +} + +impl From for DetectionError { + fn from(error: PluginError) -> Self { + Self::Model(error) + } +} + pub(super) struct RampartDetector { tokenizer: RampartTokenizer, // Each run creates invocation-local tract state. The sanitizer bounds how @@ -189,11 +209,13 @@ impl RampartDetector { max_windows_per_payload, inference_batch_size, }; - detector.detect(&["warmup"])?; + detector + .detect(&["warmup"]) + .map_err(DetectionError::into_plugin_error)?; Ok(detector) } - pub(super) fn detect(&self, texts: &[&str]) -> PluginResult> { + pub(super) fn detect(&self, texts: &[&str]) -> Result, DetectionError> { let prepared = texts .iter() .map(|text| PreparedText::new(text)) @@ -249,7 +271,8 @@ impl RampartDetector { let Some((start, end)) = prepared[text_index].project(span.start, span.end) else { return Err(inference_error( "Rampart prefilter returned an invalid UTF-8 span", - )); + ) + .into()); }; span.start = start; span.end = end; @@ -264,7 +287,8 @@ impl RampartDetector { { return Err(inference_error( "Rampart tokenizer returned an invalid UTF-8 span", - )); + ) + .into()); } detections.push(Detection { text_index, @@ -278,7 +302,7 @@ impl RampartDetector { Ok(detections) } - fn build_windows(&self, texts: &[&str]) -> PluginResult> { + fn build_windows(&self, texts: &[&str]) -> Result, DetectionError> { let step = CONTENT_TOKEN_BUDGET - WINDOW_OVERLAP_TOKENS; let mut windows = Vec::new(); for (text_index, text) in texts.iter().copied().enumerate() { @@ -309,10 +333,7 @@ impl RampartDetector { offsets: window_offsets, }); if windows.len() > self.max_windows_per_payload { - return Err(inference_error(format!( - "selected content exceeded max_windows_per_payload={}", - self.max_windows_per_payload - ))); + return Err(DetectionError::PayloadLimit); } if end == ids.len() { break; diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index e54fd303d..3138f0c2a 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -29,9 +29,8 @@ use crate::builtin::escape_json_pointer_segment; use crate::overlay::BuiltinCodecName; use super::RampartPiiConfig; -use super::model::{Detection, RampartDetector}; +use super::model::{Detection, DetectionError, RampartDetector}; -const MAX_TEXT_BYTES: usize = 16 * 1024; const MAX_TEXTS_PER_PAYLOAD: usize = 256; const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; // Cap CPU workers while respecting smaller hosts and container CPU quotas. @@ -48,11 +47,11 @@ fn inference_worker_count() -> usize { } pub(super) trait DetectionModel: Send + Sync { - fn detect(&self, texts: &[&str]) -> PluginResult>; + fn detect(&self, texts: &[&str]) -> Result, DetectionError>; } impl DetectionModel for RampartDetector { - fn detect(&self, texts: &[&str]) -> PluginResult> { + fn detect(&self, texts: &[&str]) -> Result, DetectionError> { RampartDetector::detect(self, texts) } } @@ -95,7 +94,12 @@ impl JsonPointerPattern { struct SelectedText { text: String, - eligible: bool, +} + +#[derive(Debug, PartialEq, Eq)] +enum SanitizeError { + Codec, + PayloadLimit, } enum EventField { @@ -218,13 +222,14 @@ impl RampartSanitizer { Json::String(self.replacement.to_string()) } - fn sanitize_json(&self, value: Json) -> Json { - self.sanitize_json_values(vec![value]) + fn sanitize_json(&self, value: Json) -> Result { + Ok(self + .sanitize_json_values(vec![value])? .pop() - .expect("single-value sanitization returns one value") + .expect("single-value sanitization returns one value")) } - fn sanitize_json_values(&self, values: Vec) -> Vec { + fn sanitize_json_values(&self, values: Vec) -> Result, SanitizeError> { self.sanitize_json_roots( values .into_iter() @@ -233,28 +238,24 @@ impl RampartSanitizer { ) } - fn sanitize_json_roots(&self, mut roots: Vec<(Vec, Json)>) -> Vec { + fn sanitize_json_roots( + &self, + mut roots: Vec<(Vec, Json)>, + ) -> Result, SanitizeError> { let mut texts = Vec::new(); let mut total_bytes = 0; - let mut within_budget = true; for (path, value) in &roots { let mut path = path.clone(); - self.collect_strings( - value, - &mut path, - &mut texts, - &mut total_bytes, - &mut within_budget, - ); + self.collect_strings(value, &mut path, &mut texts, &mut total_bytes)?; } - let sanitized = self.sanitize_texts(texts); + let sanitized = self.sanitize_texts(texts)?; let mut index = 0; for (path, value) in &mut roots { let mut path = path.clone(); self.replace_strings(value, &mut path, &sanitized, &mut index); } - roots.into_iter().map(|(_, value)| value).collect() + Ok(roots.into_iter().map(|(_, value)| value).collect()) } fn collect_strings( @@ -263,51 +264,40 @@ impl RampartSanitizer { path: &mut Vec, texts: &mut Vec, total_bytes: &mut usize, - within_budget: &mut bool, - ) { + ) -> Result<(), SanitizeError> { match value { Json::String(text) if self.matches_path(path) => { - if !*within_budget || texts.len() >= MAX_TEXTS_PER_PAYLOAD { - *within_budget = false; - return; - } - if text.len() > MAX_TEXT_BYTES { - texts.push(SelectedText { - text: self.replacement.to_string(), - eligible: false, - }); - return; + if texts.len() >= MAX_TEXTS_PER_PAYLOAD { + return Err(SanitizeError::PayloadLimit); } let Some(next_total) = total_bytes.checked_add(text.len()) else { - *within_budget = false; - return; + return Err(SanitizeError::PayloadLimit); }; if next_total > MAX_PAYLOAD_TEXT_BYTES { - *within_budget = false; - return; + return Err(SanitizeError::PayloadLimit); } *total_bytes = next_total; - texts.push(SelectedText { - text: text.clone(), - eligible: true, - }); + texts.push(SelectedText { text: text.clone() }); } Json::Array(items) => { for (index, item) in items.iter().enumerate() { path.push(index.to_string()); - self.collect_strings(item, path, texts, total_bytes, within_budget); + let result = self.collect_strings(item, path, texts, total_bytes); path.pop(); + result?; } } Json::Object(fields) => { for (key, value) in fields { path.push(escape_json_pointer_segment(key)); - self.collect_strings(value, path, texts, total_bytes, within_budget); + let result = self.collect_strings(value, path, texts, total_bytes); path.pop(); + result?; } } _ => {} } + Ok(()) } fn has_selected_string(&self, value: &Json) -> bool { @@ -374,29 +364,37 @@ impl RampartSanitizer { .any(|pattern| pattern.matches(path)) } - fn sanitize_texts(&self, mut texts: Vec) -> Vec { - let eligible = texts - .iter() - .enumerate() - .filter_map(|(index, text)| text.eligible.then_some(index)) - .collect::>(); - if !eligible.is_empty() && self.sanitize_batch(&mut texts, &eligible).is_err() { - log::warn!( - target: "nemo_relay.plugin", - event = "rampart_pii_inference_failed", - plugin_kind = super::RAMPART_PII_PLUGIN_KIND, - selected_text_count = eligible.len(), - reason = "model_or_output"; - "Rampart PII inference failed closed" - ); - for index in eligible { - texts[index].text = self.replacement.to_string(); + fn sanitize_texts(&self, mut texts: Vec) -> Result, SanitizeError> { + let selected = (0..texts.len()).collect::>(); + if !selected.is_empty() { + match self.sanitize_batch(&mut texts, &selected) { + Ok(()) => {} + Err(DetectionError::PayloadLimit) => { + return Err(SanitizeError::PayloadLimit); + } + Err(DetectionError::Model(_)) => { + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + selected_text_count = selected.len(), + reason = "model_or_output"; + "Rampart PII inference failed closed" + ); + for index in selected { + texts[index].text = self.replacement.to_string(); + } + } } } - texts.into_iter().map(|selected| selected.text).collect() + Ok(texts.into_iter().map(|selected| selected.text).collect()) } - fn sanitize_batch(&self, texts: &mut [SelectedText], batch: &[usize]) -> PluginResult<()> { + fn sanitize_batch( + &self, + texts: &mut [SelectedText], + batch: &[usize], + ) -> Result<(), DetectionError> { let selected = batch .iter() .map(|index| texts[*index].text.as_str()) @@ -408,9 +406,9 @@ impl RampartSanitizer { || !detection.score.is_finite() || !(0.0..=1.0).contains(&detection.score) { - return Err(PluginError::Internal( - "Rampart returned an invalid detection".into(), - )); + return Err( + PluginError::Internal("Rampart returned an invalid detection".into()).into(), + ); } by_text[detection.text_index].push(detection); } @@ -435,7 +433,8 @@ impl RampartSanitizer { { return Err(PluginError::Internal( "Rampart returned invalid or overlapping UTF-8 spans".into(), - )); + ) + .into()); } previous_end = detection.end_utf8; } @@ -455,40 +454,43 @@ impl RampartSanitizer { &self, codec: &dyn LlmCodec, request: &LlmRequest, - ) -> Option { - let annotated = codec.decode(request).ok()?; - let annotated = serde_json::to_value(annotated).ok()?; + ) -> Result { + let annotated = codec.decode(request).map_err(|_| SanitizeError::Codec)?; + let annotated = serde_json::to_value(annotated).map_err(|_| SanitizeError::Codec)?; let (headers, annotated) = self.sanitize_request_parts(request.headers.clone(), annotated)?; - let annotated = serde_json::from_value(annotated).ok()?; - let mut encoded = codec.encode(&annotated, request).ok()?; + let annotated = serde_json::from_value(annotated).map_err(|_| SanitizeError::Codec)?; + let mut encoded = codec + .encode(&annotated, request) + .map_err(|_| SanitizeError::Codec)?; encoded.headers = headers; - Some(encoded) + Ok(encoded) } - fn sanitize_raw_request(&self, mut request: LlmRequest) -> Option { + fn sanitize_raw_request(&self, mut request: LlmRequest) -> Result { let headers = std::mem::take(&mut request.headers); let content = std::mem::take(&mut request.content); let (headers, content) = self.sanitize_request_parts(headers, content)?; request.headers = headers; request.content = content; - Some(request) + Ok(request) } fn sanitize_request_parts( &self, headers: Map, content: Json, - ) -> Option<(Map, Json)> { + ) -> Result<(Map, Json), SanitizeError> { let mut values = self.sanitize_json_roots(vec![ (vec!["headers".to_string()], Json::Object(headers)), (Vec::new(), content), - ]); - let content = values.pop()?; - let Json::Object(headers) = values.pop()? else { - return None; + ])?; + let content = values.pop().ok_or(SanitizeError::Codec)?; + let headers = values.pop().ok_or(SanitizeError::Codec)?; + let Json::Object(headers) = headers else { + return Err(SanitizeError::Codec); }; - Some((headers, content)) + Ok((headers, content)) } fn sanitize_response_with_codec( @@ -496,7 +498,7 @@ impl RampartSanitizer { codec: &dyn LlmResponseCodec, surface: ProviderSurface, payload: Json, - ) -> Option { + ) -> Result { if surface == ProviderSurface::OpenAIChat && payload .get("choices") @@ -504,12 +506,14 @@ impl RampartSanitizer { .is_some_and(|choices| choices.len() > 1) && self.targets_normalized_openai_chat_choice() { - return None; + return Err(SanitizeError::Codec); } let codec_name = BuiltinCodecName::from_provider_surface(surface); - let annotated = codec.decode_response(&payload).ok()?; - let sanitized = sanitize_serializable(self, annotated).ok()?; - Some(codec_name.overlay_response_payload(payload, &sanitized)) + let annotated = codec + .decode_response(&payload) + .map_err(|_| SanitizeError::Codec)?; + let sanitized = sanitize_serializable(self, annotated)?; + Ok(codec_name.overlay_response_payload(payload, &sanitized)) } fn targets_normalized_openai_chat_choice(&self) -> bool { @@ -626,10 +630,8 @@ pub(super) fn event_sanitize_callback( .iter_mut() .map(|(_, value)| std::mem::take(value)) .collect(); - for ((field, _), value) in selected - .into_iter() - .zip(backend.sanitize_json_values(values)) - { + let sanitized_values = backend.sanitize_json_values(values)?; + for ((field, _), value) in selected.into_iter().zip(sanitized_values) { match field { EventField::Data => fields.data = Some(value), EventField::CategoryProfile => { @@ -638,7 +640,7 @@ pub(super) fn event_sanitize_callback( EventField::Metadata => fields.metadata = Some(value), } } - fields + Ok(fields) }) .await }) @@ -653,24 +655,30 @@ pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSan return Ok(None); }; run_inference("LLM request", permit, None, move || { - if matches!(context.codec(), LlmCodecIdentity::None) + let sanitized = if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - return backend.sanitize_raw_request(request); - } - let resolved = context.resolve_codec(); - let fallback = if resolved.is_none() { - backend - .selected_surface(context.codec()) - .map(build_request_codec) + backend.sanitize_raw_request(request).map(Some) } else { - None + let resolved = context.resolve_codec(); + let fallback = if resolved.is_none() { + backend + .selected_surface(context.codec()) + .map(build_request_codec) + } else { + None + }; + resolved + .as_deref() + .or(fallback.as_deref()) + .ok_or(SanitizeError::Codec) + .and_then(|codec| { + backend + .sanitize_request_with_codec(codec, &request) + .map(Some) + }) }; - let sanitized = resolved - .as_deref() - .or(fallback.as_deref()) - .and_then(|codec| backend.sanitize_request_with_codec(codec, &request)); - if sanitized.is_none() { + if matches!(sanitized, Err(SanitizeError::Codec)) { backend.log_codec_failure( "request", context.codec(), @@ -695,7 +703,7 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { - return Some(backend.sanitize_json(payload)); + return backend.sanitize_json(payload).map(Some); } if matches!(context.codec(), LlmCodecIdentity::None) && !backend.uses_compatible_legacy_response_codec(&payload) @@ -705,7 +713,7 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa context.codec(), "no compatible legacy codec", ); - return None; + return Err(SanitizeError::Codec); } let surface = backend.selected_surface(context.codec()); let resolved = context.resolve_codec(); @@ -716,10 +724,13 @@ pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSa }; let sanitized = surface .zip(resolved.as_deref().or(fallback.as_deref())) + .ok_or(SanitizeError::Codec) .and_then(|(surface, codec)| { - backend.sanitize_response_with_codec(codec, surface, payload) + backend + .sanitize_response_with_codec(codec, surface, payload) + .map(Some) }); - if sanitized.is_none() { + if matches!(sanitized, Err(SanitizeError::Codec)) { backend.log_codec_failure( "response", context.codec(), @@ -737,7 +748,7 @@ async fn run_inference( target: &'static str, permit: SanitizerPermit, fallback: T, - operation: impl FnOnce() -> T + Send + 'static, + operation: impl FnOnce() -> Result + Send + 'static, ) -> FlowResult where T: Send + 'static, @@ -749,7 +760,19 @@ where let _ = sender.send(catch_unwind(AssertUnwindSafe(operation))); }); match receiver.await { - Ok(Ok(value)) => Ok(value), + Ok(Ok(Ok(value))) => Ok(value), + Ok(Ok(Err(SanitizeError::PayloadLimit))) => { + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + reason = "payload_limit", + target; + "Rampart PII sanitization exceeded a payload limit and failed closed" + ); + Ok(fallback) + } + Ok(Ok(Err(SanitizeError::Codec))) => Ok(fallback), Ok(Err(_)) => { log::error!( target: "nemo_relay.plugin", @@ -845,12 +868,12 @@ fn compile_json_pointer(pointer: String) -> Vec { }) } -fn sanitize_serializable(backend: &RampartSanitizer, value: T) -> PluginResult +fn sanitize_serializable(backend: &RampartSanitizer, value: T) -> Result where T: Serialize + DeserializeOwned, { - let value = serde_json::to_value(value)?; - serde_json::from_value(backend.sanitize_json(value)).map_err(PluginError::from) + let value = serde_json::to_value(value).map_err(|_| SanitizeError::Codec)?; + serde_json::from_value(backend.sanitize_json(value)?).map_err(|_| SanitizeError::Codec) } #[cfg(test)] @@ -863,7 +886,7 @@ mod tests { struct NameDetector; impl DetectionModel for NameDetector { - fn detect(&self, texts: &[&str]) -> PluginResult> { + fn detect(&self, texts: &[&str]) -> Result, DetectionError> { Ok(texts .iter() .enumerate() @@ -883,15 +906,23 @@ mod tests { struct FailingDetector; impl DetectionModel for FailingDetector { - fn detect(&self, _texts: &[&str]) -> PluginResult> { - Err(PluginError::Internal("model failure".into())) + fn detect(&self, _texts: &[&str]) -> Result, DetectionError> { + Err(PluginError::Internal("model failure".into()).into()) + } + } + + struct PayloadLimitedDetector; + + impl DetectionModel for PayloadLimitedDetector { + fn detect(&self, _texts: &[&str]) -> Result, DetectionError> { + Err(DetectionError::PayloadLimit) } } struct PanickingDetector; impl DetectionModel for PanickingDetector { - fn detect(&self, _texts: &[&str]) -> PluginResult> { + fn detect(&self, _texts: &[&str]) -> Result, DetectionError> { panic!("model panic") } } @@ -899,7 +930,7 @@ mod tests { struct CountingDetector(Arc); impl DetectionModel for CountingDetector { - fn detect(&self, _texts: &[&str]) -> PluginResult> { + fn detect(&self, _texts: &[&str]) -> Result, DetectionError> { self.0.fetch_add(1, Ordering::Relaxed); Ok(Vec::new()) } @@ -912,7 +943,7 @@ mod tests { } impl DetectionModel for BlockingDetector { - fn detect(&self, _texts: &[&str]) -> PluginResult> { + fn detect(&self, _texts: &[&str]) -> Result, DetectionError> { self.started.store(true, Ordering::Release); while !self.release.load(Ordering::Acquire) { std::thread::sleep(Duration::from_millis(1)); @@ -928,7 +959,7 @@ mod tests { } impl DetectionModel for CountingBlockingDetector { - fn detect(&self, _texts: &[&str]) -> PluginResult> { + fn detect(&self, _texts: &[&str]) -> Result, DetectionError> { self.started.fetch_add(1, Ordering::Release); while !self.release.load(Ordering::Acquire) { std::thread::sleep(Duration::from_millis(1)); @@ -961,7 +992,7 @@ mod tests { "model": "model-José" }); assert_eq!( - sanitizer.sanitize_json(value), + sanitizer.sanitize_json(value).unwrap(), serde_json::json!({ "messages": [{"content": "Hello [REDACTED] Rivera"}], "message": "[REDACTED]", @@ -974,10 +1005,12 @@ mod tests { fn model_errors_fail_closed_only_for_selected_values() { let sanitizer = sanitizer(Arc::new(FailingDetector), vec!["/message"]); assert_eq!( - sanitizer.sanitize_json(serde_json::json!({ - "message": "private", - "metadata": "visible" - })), + sanitizer + .sanitize_json(serde_json::json!({ + "message": "private", + "metadata": "visible" + })) + .unwrap(), serde_json::json!({ "message": "[REDACTED]", "metadata": "visible" @@ -986,25 +1019,86 @@ mod tests { } #[test] - fn selected_values_over_payload_budget_fail_closed() { + fn sparse_selected_field_above_16_kib_is_sanitized() { + let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/message"]); + let message = format!("{}José", " ".repeat(16 * 1024)); + let sanitized = sanitizer + .sanitize_json(serde_json::json!({ + "message": message, + "metadata": "visible" + })) + .unwrap(); + let sanitized_message = sanitized["message"].as_str().unwrap(); + + assert_eq!(sanitized_message.len(), 16 * 1024 + "[REDACTED]".len()); + assert!(sanitized_message.ends_with("[REDACTED]")); + assert_eq!(sanitized["metadata"], "visible"); + } + + #[test] + fn selected_text_count_limit_rejects_the_entire_payload() { let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/*"]); let value = Json::Object( (0..=MAX_TEXTS_PER_PAYLOAD) .map(|index| (index.to_string(), Json::String("safe".into()))) .collect(), ); - let sanitized = sanitizer.sanitize_json(value); assert_eq!( - sanitized - .as_object() - .unwrap() - .values() - .filter(|value| **value == "[REDACTED]") - .count(), - 1 + sanitizer.sanitize_json(value), + Err(SanitizeError::PayloadLimit) ); } + #[test] + fn selected_payload_byte_limit_has_an_exact_boundary() { + let calls = Arc::new(AtomicUsize::new(0)); + let sanitizer = sanitizer( + Arc::new(CountingDetector(Arc::clone(&calls))), + vec!["/message"], + ); + let exact = " ".repeat(MAX_PAYLOAD_TEXT_BYTES); + let sanitized = sanitizer + .sanitize_json(serde_json::json!({"message": exact})) + .unwrap(); + assert_eq!( + sanitized["message"].as_str().unwrap().len(), + MAX_PAYLOAD_TEXT_BYTES + ); + assert_eq!(calls.load(Ordering::Relaxed), 1); + + assert_eq!( + sanitizer.sanitize_json(serde_json::json!({ + "message": " ".repeat(MAX_PAYLOAD_TEXT_BYTES + 1) + })), + Err(SanitizeError::PayloadLimit) + ); + assert_eq!(calls.load(Ordering::Relaxed), 1); + } + + #[tokio::test(flavor = "current_thread")] + async fn aggregate_limit_fails_closed_before_partial_tool_sanitization() { + let calls = Arc::new(AtomicUsize::new(0)); + let backend = sanitizer( + Arc::new(CountingDetector(Arc::clone(&calls))), + vec!["/messages/*/content"], + ); + let output = tool_sanitize_callback(backend)( + "tool".into(), + serde_json::json!({ + "messages": [ + {"content": "first-private-value"}, + {"content": " ".repeat(MAX_PAYLOAD_TEXT_BYTES)} + ], + "metadata": "visible" + }), + ) + .await + .unwrap(); + + assert_eq!(output, Json::String("[REDACTED]".into())); + assert_eq!(calls.load(Ordering::Relaxed), 0); + } + #[test] fn selected_payload_uses_one_detector_call() { let calls = Arc::new(AtomicUsize::new(0)); @@ -1015,7 +1109,12 @@ mod tests { .collect(), ); assert_eq!( - sanitizer.sanitize_json(value).as_object().unwrap().len(), + sanitizer + .sanitize_json(value) + .unwrap() + .as_object() + .unwrap() + .len(), 128 ); assert_eq!(calls.load(Ordering::Relaxed), 1); @@ -1229,6 +1328,66 @@ mod tests { ); } + #[tokio::test(flavor = "current_thread")] + async fn model_window_limit_fails_closed_for_every_surface() { + use nemo_relay::api::event::{BaseEvent, MarkEvent}; + use nemo_relay::api::runtime::{LlmSanitizeRequestContext, LlmSanitizeResponseContext}; + + let backend = sanitizer(Arc::new(PayloadLimitedDetector), vec!["/message"]); + let private = "must-not-pass-through"; + assert_eq!( + tool_sanitize_callback(backend.clone())( + "tool".into(), + serde_json::json!({"message": private, "metadata": "visible"}), + ) + .await + .unwrap(), + Json::String("[REDACTED]".into()) + ); + + let event = Arc::new(Event::Mark(MarkEvent::new( + BaseEvent::builder() + .name("mark") + .data(serde_json::json!({"message": private})) + .metadata(serde_json::json!({"message": private})) + .build(), + None, + None, + ))); + assert_eq!( + event_sanitize_callback(backend.clone(), None)( + Arc::clone(&event), + event.sanitize_fields(), + ) + .await + .unwrap(), + EventSanitizeFields::default() + ); + + let request = LlmRequest { + headers: Map::new(), + content: serde_json::json!({"message": private}), + }; + assert!( + llm_sanitize_request_callback(backend.clone())( + request, + LlmSanitizeRequestContext::default(), + ) + .await + .unwrap() + .is_none() + ); + assert!( + llm_sanitize_response_callback(backend)( + serde_json::json!({"message": private}), + LlmSanitizeResponseContext::default(), + ) + .await + .unwrap() + .is_none() + ); + } + #[test] fn bounded_admission_times_out_before_spawning_more_blocking_work() { let worker_count = inference_worker_count(); @@ -1600,19 +1759,21 @@ mod tests { }); let codec = build_response_codec(ProviderSurface::OpenAIChat); - assert!( - exact - .sanitize_response_with_codec( - codec.as_ref(), - ProviderSurface::OpenAIChat, - payload.clone(), - ) - .is_none() + assert_eq!( + exact.sanitize_response_with_codec( + codec.as_ref(), + ProviderSurface::OpenAIChat, + payload.clone(), + ), + Err(SanitizeError::Codec) ); - assert!( - wildcard - .sanitize_response_with_codec(codec.as_ref(), ProviderSurface::OpenAIChat, payload) - .is_none() + assert_eq!( + wildcard.sanitize_response_with_codec( + codec.as_ref(), + ProviderSurface::OpenAIChat, + payload, + ), + Err(SanitizeError::Codec) ); } diff --git a/go/nemo_relay/pii_rampart.go b/go/nemo_relay/pii_rampart.go index e1c476158..220779b66 100644 --- a/go/nemo_relay/pii_rampart.go +++ b/go/nemo_relay/pii_rampart.go @@ -56,7 +56,7 @@ func NewRampartPiiConfig(modelPath string) RampartPiiConfig { MinScore: 0.4, ExcludedLabels: []string{}, Replacement: "[REDACTED]", - MaxWindowsPerPayload: 128, + MaxWindowsPerPayload: 4, InferenceBatchSize: 16, } } diff --git a/go/nemo_relay/pii_rampart_test.go b/go/nemo_relay/pii_rampart_test.go index c5d2be44c..c0353a675 100644 --- a/go/nemo_relay/pii_rampart_test.go +++ b/go/nemo_relay/pii_rampart_test.go @@ -18,6 +18,9 @@ func TestRampartPiiConfigHelpers(t *testing.T) { if component.Config["model_path"] != "/models/rampart" { t.Fatalf("unexpected Rampart PII config: %#v", component.Config) } + if config.MaxWindowsPerPayload != 4 { + t.Fatalf("unexpected Rampart PII window limit: %d", config.MaxWindowsPerPayload) + } if RampartModelID != "nationaldesignstudio/rampart" || RampartModelRevision != "b1993e4e68b082835b80ffc65acc03325ea2e501" { t.Fatalf("unexpected Rampart model identity: %s@%s", RampartModelID, RampartModelRevision) diff --git a/python/nemo_relay/pii_rampart.py b/python/nemo_relay/pii_rampart.py index a6b2a620b..c2a51272c 100644 --- a/python/nemo_relay/pii_rampart.py +++ b/python/nemo_relay/pii_rampart.py @@ -37,7 +37,7 @@ class RampartPiiConfig: min_score: float = 0.4 excluded_labels: list[str] = field(default_factory=list) replacement: str = "[REDACTED]" - max_windows_per_payload: int = 128 + max_windows_per_payload: int = 4 inference_batch_size: int = 16 policy: ConfigPolicy = field(default_factory=ConfigPolicy) diff --git a/python/tests/test_pii_rampart_plugin.py b/python/tests/test_pii_rampart_plugin.py index eafbcc4dc..e8bbabe30 100644 --- a/python/tests/test_pii_rampart_plugin.py +++ b/python/tests/test_pii_rampart_plugin.py @@ -20,6 +20,7 @@ def test_rampart_config_and_component_shape() -> None: ) value = config.to_dict() assert value["model_path"] == "/models/rampart" + assert value["max_windows_per_payload"] == 4 assert value["inference_batch_size"] == 16 assert RAMPART_MODEL_ID == "nationaldesignstudio/rampart" assert RAMPART_MODEL_REVISION == "b1993e4e68b082835b80ffc65acc03325ea2e501" From 26644e94935d48cf0dee4ef9964da483c9a1a332 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 3 Aug 2026 12:52:45 -0700 Subject: [PATCH 80/83] test(pii): address Rampart review nits Signed-off-by: Alex Fournier --- crates/node/pii_rampart.js | 1 - crates/pii-redaction/src/rampart/mod.rs | 4 ++- crates/pii-redaction/src/rampart/sanitizer.rs | 25 +++++++++++++ go/nemo_relay/pii_rampart/pii_rampart_test.go | 35 ++++++++++++++++++- go/nemo_relay/pii_rampart_test.go | 31 ++++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/crates/node/pii_rampart.js b/crates/node/pii_rampart.js index 2cbaed61c..15a42f235 100644 --- a/crates/node/pii_rampart.js +++ b/crates/node/pii_rampart.js @@ -28,7 +28,6 @@ function defaultConfig(modelPath, config) { } return { version: 1, - model_path: modelPath, input: true, output: true, mark: true, diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs index 2eac3f3e4..11414fcd8 100644 --- a/crates/pii-redaction/src/rampart/mod.rs +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -127,7 +127,9 @@ pub struct RampartPiiConfig { /// Maximum token windows accepted from one observability payload. #[serde(default = "default_max_windows_per_payload")] pub max_windows_per_payload: usize, - /// Maximum short token windows grouped in one model invocation. + /// Maximum short token windows requested per model invocation. + /// + /// The 512 padded-token budget can reduce the actual batch size. #[serde(default = "default_inference_batch_size")] pub inference_batch_size: usize, /// Component-local unsupported-config policy. diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index 3138f0c2a..3e7c1b636 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -1001,6 +1001,31 @@ mod tests { ); } + #[test] + fn exact_selectors_match_escaped_json_pointer_segments() { + let sanitizer = RampartSanitizer::new( + RampartPiiConfig { + model_path: "/tmp/rampart".into(), + target_paths: vec!["/a~1b/c~0d".into()], + ..RampartPiiConfig::default() + }, + Arc::new(NameDetector), + ) + .unwrap(); + let value = serde_json::json!({ + "a/b": {"c~d": "Hello José"}, + "a": {"b": {"c~d": "José"}} + }); + + assert_eq!( + sanitizer.sanitize_json(value).unwrap(), + serde_json::json!({ + "a/b": {"c~d": "Hello [REDACTED]"}, + "a": {"b": {"c~d": "José"}} + }) + ); + } + #[test] fn model_errors_fail_closed_only_for_selected_values() { let sanitizer = sanitizer(Arc::new(FailingDetector), vec!["/message"]); diff --git a/go/nemo_relay/pii_rampart/pii_rampart_test.go b/go/nemo_relay/pii_rampart/pii_rampart_test.go index 371a8e6a4..a361ffd99 100644 --- a/go/nemo_relay/pii_rampart/pii_rampart_test.go +++ b/go/nemo_relay/pii_rampart/pii_rampart_test.go @@ -3,7 +3,10 @@ package pii_rampart -import "testing" +import ( + "path/filepath" + "testing" +) func TestConfigAndComponentHelpers(t *testing.T) { config := NewConfig("/models/rampart") @@ -26,3 +29,33 @@ func TestConfigAndComponentHelpers(t *testing.T) { t.Fatal("disabled Rampart PII component was enabled") } } + +func TestValidateConfig(t *testing.T) { + modelPath, err := filepath.Abs("testdata/rampart") + if err != nil { + t.Fatalf("resolve model path: %v", err) + } + config := NewConfig(modelPath) + config.TargetPaths = []string{"/message"} + + report, err := ValidateConfig(config) + if err != nil { + t.Fatalf("ValidateConfig failed: %v", err) + } + if len(report.Diagnostics) != 0 { + t.Fatalf("unexpected diagnostics: %#v", report.Diagnostics) + } + + config.TargetPaths = nil + config.TargetPathPatterns = []string{"/messages/pre*fix/content"} + report, err = ValidateConfig(config) + if err != nil { + t.Fatalf("ValidateConfig rejected diagnostic input: %v", err) + } + for _, diagnostic := range report.Diagnostics { + if diagnostic.Field != nil && *diagnostic.Field == "target_path_patterns" { + return + } + } + t.Fatalf("expected target_path_patterns diagnostic, got %#v", report.Diagnostics) +} diff --git a/go/nemo_relay/pii_rampart_test.go b/go/nemo_relay/pii_rampart_test.go index c0353a675..a3ff1a5b5 100644 --- a/go/nemo_relay/pii_rampart_test.go +++ b/go/nemo_relay/pii_rampart_test.go @@ -5,6 +5,7 @@ package nemo_relay import ( "encoding/json" + "path/filepath" "testing" ) @@ -50,3 +51,33 @@ func TestRampartPiiConfigPreservesExplicitZeroValues(t *testing.T) { t.Fatalf("explicit zero values were not preserved: %#v", value) } } + +func TestValidateRampartPiiConfig(t *testing.T) { + modelPath, err := filepath.Abs("testdata/rampart") + if err != nil { + t.Fatalf("resolve model path: %v", err) + } + config := NewRampartPiiConfig(modelPath) + config.TargetPaths = []string{"/message"} + + report, err := ValidateRampartPiiConfig(config) + if err != nil { + t.Fatalf("ValidateRampartPiiConfig failed: %v", err) + } + if len(report.Diagnostics) != 0 { + t.Fatalf("unexpected diagnostics: %#v", report.Diagnostics) + } + + config.TargetPaths = nil + config.TargetPathPatterns = []string{"/messages/pre*fix/content"} + report, err = ValidateRampartPiiConfig(config) + if err != nil { + t.Fatalf("ValidateRampartPiiConfig rejected diagnostic input: %v", err) + } + for _, diagnostic := range report.Diagnostics { + if diagnostic.Field != nil && *diagnostic.Field == "target_path_patterns" { + return + } + } + t.Fatalf("expected target_path_patterns diagnostic, got %#v", report.Diagnostics) +} From 1866014149a331f05a939f2fbd1c44d22c246b79 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 3 Aug 2026 12:56:46 -0700 Subject: [PATCH 81/83] refactor(pii): simplify Rampart batch sanitization Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/mod.rs | 7 +++ crates/pii-redaction/src/rampart/model.rs | 4 +- crates/pii-redaction/src/rampart/sanitizer.rs | 53 ++++++++++++------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs index 11414fcd8..71de4161b 100644 --- a/crates/pii-redaction/src/rampart/mod.rs +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -726,6 +726,13 @@ mod tests { .iter() .any(|item| item.field.as_deref() == Some("max_windows_per_payload")) ); + + config.insert("max_windows_per_payload".into(), Json::from(0_usize)); + assert!( + validate_rampart_pii_config(&config, None) + .iter() + .any(|item| item.field.as_deref() == Some("max_windows_per_payload")) + ); } #[tokio::test] diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index 93468edf8..2478ff90b 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -64,7 +64,7 @@ pub(super) enum DetectionError { } impl DetectionError { - fn into_plugin_error(self) -> PluginError { + fn into_warmup_error(self) -> PluginError { match self { Self::PayloadLimit => inference_error("Rampart warmup exceeded its payload limit"), Self::Model(error) => error, @@ -211,7 +211,7 @@ impl RampartDetector { }; detector .detect(&["warmup"]) - .map_err(DetectionError::into_plugin_error)?; + .map_err(DetectionError::into_warmup_error)?; Ok(detector) } diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index 3e7c1b636..7458e0e80 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -365,9 +365,9 @@ impl RampartSanitizer { } fn sanitize_texts(&self, mut texts: Vec) -> Result, SanitizeError> { - let selected = (0..texts.len()).collect::>(); - if !selected.is_empty() { - match self.sanitize_batch(&mut texts, &selected) { + if !texts.is_empty() { + let selected_text_count = texts.len(); + match self.sanitize_batch(&mut texts) { Ok(()) => {} Err(DetectionError::PayloadLimit) => { return Err(SanitizeError::PayloadLimit); @@ -377,12 +377,12 @@ impl RampartSanitizer { target: "nemo_relay.plugin", event = "rampart_pii_inference_failed", plugin_kind = super::RAMPART_PII_PLUGIN_KIND, - selected_text_count = selected.len(), + selected_text_count, reason = "model_or_output"; "Rampart PII inference failed closed" ); - for index in selected { - texts[index].text = self.replacement.to_string(); + for selected in &mut texts { + selected.text = self.replacement.to_string(); } } } @@ -390,19 +390,15 @@ impl RampartSanitizer { Ok(texts.into_iter().map(|selected| selected.text).collect()) } - fn sanitize_batch( - &self, - texts: &mut [SelectedText], - batch: &[usize], - ) -> Result<(), DetectionError> { - let selected = batch + fn sanitize_batch(&self, texts: &mut [SelectedText]) -> Result<(), DetectionError> { + let selected = texts .iter() - .map(|index| texts[*index].text.as_str()) + .map(|selected| selected.text.as_str()) .collect::>(); let detections = self.detector.detect(&selected)?; - let mut by_text = vec![Vec::::new(); batch.len()]; + let mut by_text = vec![Vec::::new(); texts.len()]; for detection in detections { - if detection.text_index >= batch.len() + if detection.text_index >= texts.len() || !detection.score.is_finite() || !(0.0..=1.0).contains(&detection.score) { @@ -413,7 +409,7 @@ impl RampartSanitizer { by_text[detection.text_index].push(detection); } - for (original_index, mut detections) in batch.iter().copied().zip(by_text) { + for (selected, mut detections) in texts.iter_mut().zip(by_text) { detections.retain(|detection| { detection.score >= self.min_score && !self.excluded_labels.contains(&detection.label) @@ -422,7 +418,7 @@ impl RampartSanitizer { continue; } detections.sort_by_key(|detection| (detection.start_utf8, detection.end_utf8)); - let text = &texts[original_index].text; + let text = &selected.text; let mut previous_end = 0; for detection in &detections { if detection.start_utf8 >= detection.end_utf8 @@ -445,7 +441,7 @@ impl RampartSanitizer { self.replacement.as_ref(), ); } - texts[original_index].text = redacted; + selected.text = redacted; } Ok(()) } @@ -1403,7 +1399,7 @@ mod tests { .is_none() ); assert!( - llm_sanitize_response_callback(backend)( + llm_sanitize_response_callback(backend.clone())( serde_json::json!({"message": private}), LlmSanitizeResponseContext::default(), ) @@ -1411,6 +1407,25 @@ mod tests { .unwrap() .is_none() ); + + let codec = build_response_codec(ProviderSurface::OpenAIChat); + let payload = serde_json::json!({ + "id": "chatcmpl-payload-limit", + "model": "model", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": private}, + "finish_reason": "stop" + }] + }); + assert_eq!( + backend.sanitize_response_with_codec( + codec.as_ref(), + ProviderSurface::OpenAIChat, + payload, + ), + Err(SanitizeError::PayloadLimit) + ); } #[test] From 75da4a755d4aa58234da4deac2d305771d596d21 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 3 Aug 2026 16:13:39 -0700 Subject: [PATCH 82/83] fix(pii): sanitize provider-native trajectory content Signed-off-by: Alex Fournier --- crates/node/pii_rampart.d.ts | 15 +- crates/node/pii_rampart.js | 17 +- crates/node/tests/pii_rampart_tests.mjs | 22 +- crates/pii-redaction/README.md | 47 +- crates/pii-redaction/src/rampart/mod.rs | 145 ++++- crates/pii-redaction/src/rampart/model.rs | 60 ++- crates/pii-redaction/src/rampart/sanitizer.rs | 510 +++++++++++++++--- crates/pii-redaction/src/trajectory.rs | 8 +- go/nemo_relay/pii_rampart.go | 69 +-- go/nemo_relay/pii_rampart/pii_rampart.go | 2 +- go/nemo_relay/pii_rampart/pii_rampart_test.go | 17 + go/nemo_relay/pii_rampart_test.go | 20 + python/nemo_relay/pii_rampart.py | 5 + python/nemo_relay/pii_rampart.pyi | 2 + python/tests/test_pii_rampart_plugin.py | 11 + 15 files changed, 811 insertions(+), 139 deletions(-) diff --git a/crates/node/pii_rampart.d.ts b/crates/node/pii_rampart.d.ts index 8c06f7af4..2603ade35 100644 --- a/crates/node/pii_rampart.d.ts +++ b/crates/node/pii_rampart.d.ts @@ -17,11 +17,13 @@ export interface Config { tool_output?: boolean; priority?: number; codec?: 'openai_chat' | 'openai_responses' | 'anthropic_messages' | string; + preset?: 'trajectory_context' | string; target_paths?: string[]; target_path_patterns?: string[]; min_score?: number; excluded_labels?: string[]; replacement?: string; + custom_mark_payload_policy?: 'preserve' | 'redact_all_leaves' | string; max_windows_per_payload?: number; inference_batch_size?: number; policy?: ConfigPolicy; @@ -29,19 +31,22 @@ export interface Config { type NonEmptyStringArray = [string, ...string[]]; -export type ConfigWithSelectors = Omit< +export type ConfigWithSelection = Omit< Partial, - 'model_path' | 'target_paths' | 'target_path_patterns' + 'model_path' | 'preset' | 'target_paths' | 'target_path_patterns' > & ( - | { target_paths: NonEmptyStringArray; target_path_patterns?: string[] } - | { target_paths?: string[]; target_path_patterns: NonEmptyStringArray } + | { preset: 'trajectory_context'; target_paths?: never; target_path_patterns?: never } + | { preset?: never; target_paths: NonEmptyStringArray; target_path_patterns?: string[] } + | { preset?: never; target_paths?: string[]; target_path_patterns: NonEmptyStringArray } ); +export type ConfigWithSelectors = ConfigWithSelection; + export declare const RAMPART_PII_PLUGIN_KIND: 'pii_rampart'; export declare const RAMPART_MODEL_ID: 'nationaldesignstudio/rampart'; export declare const RAMPART_MODEL_REVISION: 'b1993e4e68b082835b80ffc65acc03325ea2e501'; -export declare function defaultConfig(modelPath: string, config: ConfigWithSelectors): Config; +export declare function defaultConfig(modelPath: string, config: ConfigWithSelection): Config; export declare function ComponentSpec( config: Config, options?: { enabled?: boolean }, diff --git a/crates/node/pii_rampart.js b/crates/node/pii_rampart.js index 15a42f235..e11c8a38b 100644 --- a/crates/node/pii_rampart.js +++ b/crates/node/pii_rampart.js @@ -12,19 +12,25 @@ const RAMPART_MODEL_REVISION = 'b1993e4e68b082835b80ffc65acc03325ea2e501'; /** * Create Rampart PII settings with runtime defaults applied. * - * At least one exact path or path pattern is required because Relay does not - * send unselected observability fields to the model. + * Select content with the trajectory preset or at least one exact path or path + * pattern. Relay does not send unselected observability fields to the model. * * @param {string} modelPath - Absolute path to the pinned Rampart snapshot. - * @param {object} config - Partial settings including explicit target selectors. + * @param {object} config - Partial settings including one content selection mode. * @returns {object} A normalized Rampart PII config object. */ function defaultConfig(modelPath, config) { const hasTargetPaths = Array.isArray(config?.target_paths) && config.target_paths.length > 0; const hasTargetPathPatterns = Array.isArray(config?.target_path_patterns) && config.target_path_patterns.length > 0; - if (!hasTargetPaths && !hasTargetPathPatterns) { - throw new TypeError('Rampart PII config requires target_paths or target_path_patterns'); + const hasPreset = config?.preset === 'trajectory_context'; + if (hasPreset && (hasTargetPaths || hasTargetPathPatterns)) { + throw new TypeError('Rampart PII config cannot combine preset with explicit target selectors'); + } + if (!hasPreset && !hasTargetPaths && !hasTargetPathPatterns) { + throw new TypeError( + 'Rampart PII config requires preset, target_paths, or target_path_patterns', + ); } return { version: 1, @@ -39,6 +45,7 @@ function defaultConfig(modelPath, config) { min_score: 0.4, excluded_labels: [], replacement: '[REDACTED]', + custom_mark_payload_policy: 'preserve', max_windows_per_payload: 4, inference_batch_size: 16, ...config, diff --git a/crates/node/tests/pii_rampart_tests.mjs b/crates/node/tests/pii_rampart_tests.mjs index d1c3f2782..3ee549c05 100644 --- a/crates/node/tests/pii_rampart_tests.mjs +++ b/crates/node/tests/pii_rampart_tests.mjs @@ -17,6 +17,7 @@ describe('pii_rampart plugin helpers', () => { assert.equal(config.model_path, '/models/rampart'); assert.equal(config.max_windows_per_payload, 4); assert.equal(config.inference_batch_size, 16); + assert.equal(config.custom_mark_payload_policy, 'preserve'); assert.equal(rampart.RAMPART_MODEL_ID, 'nationaldesignstudio/rampart'); assert.equal(rampart.RAMPART_MODEL_REVISION, 'b1993e4e68b082835b80ffc65acc03325ea2e501'); const component = rampart.ComponentSpec(config); @@ -26,7 +27,7 @@ describe('pii_rampart plugin helpers', () => { assert.deepEqual(rampart.validateConfig(config).diagnostics, []); }); - it('requires explicit selectors in the config helper', () => { + it('requires one content selection mode in the config helper', () => { const exact = rampart.defaultConfig('/models/rampart', { target_paths: ['/message'], }); @@ -34,11 +35,11 @@ describe('pii_rampart plugin helpers', () => { assert.throws( () => rampart.defaultConfig('/models/rampart'), - /requires target_paths or target_path_patterns/, + /requires preset, target_paths, or target_path_patterns/, ); assert.throws( () => rampart.defaultConfig('/models/rampart', {}), - /requires target_paths or target_path_patterns/, + /requires preset, target_paths, or target_path_patterns/, ); assert.throws( () => @@ -46,7 +47,20 @@ describe('pii_rampart plugin helpers', () => { target_paths: [], target_path_patterns: [], }), - /requires target_paths or target_path_patterns/, + /requires preset, target_paths, or target_path_patterns/, + ); + + const preset = rampart.defaultConfig('/models/rampart', { + preset: 'trajectory_context', + }); + assert.deepEqual(rampart.validateConfig(preset).diagnostics, []); + assert.throws( + () => + rampart.defaultConfig('/models/rampart', { + preset: 'trajectory_context', + target_paths: ['/message'], + }), + /cannot combine preset with explicit target selectors/, ); assert.equal( rampart.defaultConfig('/models/rampart', { diff --git a/crates/pii-redaction/README.md b/crates/pii-redaction/README.md index bae4bed04..09f6fee3b 100644 --- a/crates/pii-redaction/README.md +++ b/crates/pii-redaction/README.md @@ -238,9 +238,32 @@ mismatches before installing sanitizer callbacks. ### Activate the Component -Add a `pii_rampart` component with explicit content selectors. This example -sanitizes normalized LLM request and response content without sending marks, -tool payloads, or provider metadata to the model: +Use the trajectory preset to inspect conversational content while preserving +analytical fields such as provider and model identifiers, roles, status values, +tool names, and correlation IDs: + +```toml +[[components]] +kind = "pii_rampart" +enabled = true + +[components.config] +version = 1 +model_path = "/absolute/path/to/rampart" +preset = "trajectory_context" +``` + +The preset sanitizes provider-native LLM payloads without decoding and +re-encoding them through a codec. This keeps repeated message and content-block +locations distinct in long tool-use histories. It also inspects all string +leaves in tool payloads, including a root string result. Unknown custom marks +are preserved by default; set +`custom_mark_payload_policy = "redact_all_leaves"` to inspect all of their +string leaves. + +For narrower control, configure explicit content selectors instead. This +example sanitizes normalized LLM request and response content without sending +marks, tool payloads, or provider metadata to the model: ```toml [[components]] @@ -264,7 +287,8 @@ target_path_patterns = [ ``` `target_paths` contains exact JSON pointers. `target_path_patterns` also -accepts `*` as one complete path segment. At least one selector is required. +accepts `*` as one complete path segment. A preset or at least one selector is +required, and a preset cannot be combined with explicit selectors. When a supported codec is active, selectors address the normalized Relay request or response shape. For OpenAI Chat responses with multiple choices, selectors under `message`, @@ -301,13 +325,14 @@ already-sanitized specialized payload remains available. These fallbacks do not pass selected content through unsanitized. The default compute budget is four overlapping 512-token windows across all -selected strings in one payload. `max_windows_per_payload` can raise that budget -to at most 16. Relay also caps one payload at 256 selected strings and 256 KiB of -selected UTF-8 text to bound preprocessing memory. Exceeding any of these limits -logs `reason=payload_limit` and applies the same whole-surface fail-closed -behavior instead of partially sanitizing the payload. Sanitization only changes -emitted observability; it does not change arguments or return values seen by the -underlying tool or model. +selected strings in one payload. Short strings remain separate model sequences +but share that budget based on their padded inference-token volume. +`max_windows_per_payload` can raise the budget to at most 16. Relay also caps one +payload at 256 selected strings and 256 KiB of selected UTF-8 text to bound +preprocessing memory. Exceeding any of these limits logs `reason=payload_limit` +and applies the same whole-surface fail-closed behavior instead of partially +sanitizing the payload. Sanitization only changes emitted observability; it does +not change arguments or return values seen by the underlying tool or model. ## Documentation diff --git a/crates/pii-redaction/src/rampart/mod.rs b/crates/pii-redaction/src/rampart/mod.rs index 71de4161b..5d66f2718 100644 --- a/crates/pii-redaction/src/rampart/mod.rs +++ b/crates/pii-redaction/src/rampart/mod.rs @@ -109,6 +109,10 @@ pub struct RampartPiiConfig { #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "schema", schemars(schema_with = "codec_schema"))] pub codec: Option, + /// Optional semantic content-selection preset. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "schema", schemars(schema_with = "preset_schema"))] + pub preset: Option, /// Exact JSON-pointer paths selected for model inspection. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub target_paths: Vec, @@ -124,6 +128,13 @@ pub struct RampartPiiConfig { /// Replacement applied to accepted model spans and failed batches. #[serde(default = "default_replacement")] pub replacement: String, + /// How the trajectory preset handles opaque custom-mark payloads. + #[serde(default = "default_custom_mark_payload_policy")] + #[cfg_attr( + feature = "schema", + schemars(schema_with = "custom_mark_payload_policy_schema") + )] + pub custom_mark_payload_policy: String, /// Maximum token windows accepted from one observability payload. #[serde(default = "default_max_windows_per_payload")] pub max_windows_per_payload: usize, @@ -149,11 +160,13 @@ impl Default for RampartPiiConfig { tool_output: true, priority: default_priority(), codec: None, + preset: None, target_paths: Vec::new(), target_path_patterns: Vec::new(), min_score: default_min_score(), excluded_labels: Vec::new(), replacement: default_replacement(), + custom_mark_payload_policy: default_custom_mark_payload_policy(), max_windows_per_payload: default_max_windows_per_payload(), inference_batch_size: default_inference_batch_size(), policy: ConfigPolicy::default(), @@ -176,6 +189,12 @@ nemo_relay::editor_config! { values: ["openai_chat", "openai_responses", "anthropic_messages"], optional: true, }, + preset => { + label: "preset", + kind: Enum, + values: ["trajectory_context"], + optional: true, + }, target_paths => { label: "target_paths", kind: List, @@ -193,6 +212,11 @@ nemo_relay::editor_config! { list: &nemo_relay::config_editor::STRING_LIST_ITEM, }, replacement => { label: "replacement", kind: String }, + custom_mark_payload_policy => { + label: "custom_mark_payload_policy", + kind: Enum, + values: ["preserve", "redact_all_leaves"], + }, max_windows_per_payload => { label: "max_windows_per_payload", kind: Integer }, inference_batch_size => { label: "inference_batch_size", kind: Integer }, policy => { @@ -379,11 +403,13 @@ fn validate_rampart_pii_config( "tool_output", "priority", "codec", + "preset", "target_paths", "target_path_patterns", "min_score", "excluded_labels", "replacement", + "custom_mark_payload_policy", "max_windows_per_payload", "inference_batch_size", "policy", @@ -491,12 +517,7 @@ fn config_value_violations(config: &RampartPiiConfig) -> Vec { "codec must be 'openai_chat', 'openai_responses', or 'anthropic_messages'", )); } - if config.target_paths.is_empty() && config.target_path_patterns.is_empty() { - violations.push(ConfigViolation::new( - "target_paths", - "target_paths or target_path_patterns must select explicit content fields", - )); - } + validate_content_selection(config, &mut violations); if config.target_paths.len() + config.target_path_patterns.len() > MAX_TARGET_PATHS { violations.push(ConfigViolation::new( "target_paths", @@ -565,6 +586,46 @@ fn config_value_violations(config: &RampartPiiConfig) -> Vec { violations } +fn validate_content_selection(config: &RampartPiiConfig, violations: &mut Vec) { + match config.preset.as_deref() { + Some("trajectory_context") => { + if !config.target_paths.is_empty() || !config.target_path_patterns.is_empty() { + violations.push(ConfigViolation::new( + "preset", + "preset cannot be combined with target_paths or target_path_patterns", + )); + } + } + Some(_) => violations.push(ConfigViolation::new( + "preset", + "preset must be 'trajectory_context'", + )), + None => { + if config.target_paths.is_empty() && config.target_path_patterns.is_empty() { + violations.push(ConfigViolation::new( + "target_paths", + "preset, target_paths, or target_path_patterns must select content fields", + )); + } + if config.custom_mark_payload_policy != "preserve" { + violations.push(ConfigViolation::new( + "custom_mark_payload_policy", + "custom_mark_payload_policy requires preset = 'trajectory_context'", + )); + } + } + } + if !matches!( + config.custom_mark_payload_policy.as_str(), + "preserve" | "redact_all_leaves" + ) { + violations.push(ConfigViolation::new( + "custom_mark_payload_policy", + "custom_mark_payload_policy must be 'preserve' or 'redact_all_leaves'", + )); + } +} + fn push_unsupported( diagnostics: &mut Vec, config: &RampartPiiConfig, @@ -646,6 +707,10 @@ fn default_replacement() -> String { "[REDACTED]".into() } +fn default_custom_mark_payload_policy() -> String { + "preserve".into() +} + fn default_max_windows_per_payload() -> usize { 4 } @@ -667,6 +732,29 @@ fn codec_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::s schema.into() } +#[cfg(feature = "schema")] +fn preset_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + let mut schema: schemars::schema::SchemaObject = + ::json_schema(generator).into(); + schema.enum_values = Some(vec![Json::String("trajectory_context".into())]); + schema.into() +} + +#[cfg(feature = "schema")] +fn custom_mark_payload_policy_schema( + generator: &mut schemars::r#gen::SchemaGenerator, +) -> schemars::schema::Schema { + let mut schema: schemars::schema::SchemaObject = + ::json_schema(generator).into(); + schema.enum_values = Some( + ["preserve", "redact_all_leaves"] + .into_iter() + .map(|value| Json::String(value.into())) + .collect(), + ); + schema.into() +} + #[cfg(test)] mod tests { use super::*; @@ -735,6 +823,51 @@ mod tests { ); } + #[test] + fn validates_trajectory_preset_without_explicit_paths() { + let Json::Object(config) = serde_json::to_value(RampartPiiConfig { + model_path: "/tmp/rampart".into(), + preset: Some("trajectory_context".into()), + ..RampartPiiConfig::default() + }) + .unwrap() else { + unreachable!() + }; + + assert!(validate_rampart_pii_config(&config, None).is_empty()); + } + + #[test] + fn rejects_ambiguous_or_unsupported_preset_configuration() { + let mut config = valid_config(); + config.insert("preset".into(), Json::String("trajectory_context".into())); + assert!( + validate_rampart_pii_config(&config, None) + .iter() + .any(|item| item.field.as_deref() == Some("preset")) + ); + + config.remove("target_path_patterns"); + config.insert("preset".into(), Json::String("unknown".into())); + assert!( + validate_rampart_pii_config(&config, None) + .iter() + .any(|item| item.field.as_deref() == Some("preset")) + ); + + config.remove("preset"); + config.insert( + "custom_mark_payload_policy".into(), + Json::String("redact_all_leaves".into()), + ); + let diagnostics = validate_rampart_pii_config(&config, None); + assert!( + diagnostics + .iter() + .any(|item| item.field.as_deref() == Some("custom_mark_payload_policy")) + ); + } + #[tokio::test] async fn registration_enforces_safety_invariants_when_policy_warns() { let cases = [ diff --git a/crates/pii-redaction/src/rampart/model.rs b/crates/pii-redaction/src/rampart/model.rs index 2478ff90b..721399094 100644 --- a/crates/pii-redaction/src/rampart/model.rs +++ b/crates/pii-redaction/src/rampart/model.rs @@ -304,6 +304,8 @@ impl RampartDetector { fn build_windows(&self, texts: &[&str]) -> Result, DetectionError> { let step = CONTENT_TOKEN_BUDGET - WINDOW_OVERLAP_TOKENS; + let max_model_tokens = self.max_windows_per_payload * MODEL_MAX_TOKENS; + let mut modeled_tokens = 0; let mut windows = Vec::new(); for (text_index, text) in texts.iter().copied().enumerate() { let encoding = self.tokenizer.encode(text)?; @@ -326,20 +328,26 @@ impl RampartDetector { window_offsets.push(None); window_offsets.extend(offsets[start..end].iter().copied().map(Some)); window_offsets.push(None); + modeled_tokens += input_ids.len(); + if modeled_tokens > max_model_tokens { + return Err(DetectionError::PayloadLimit); + } windows.push(Window { text_index, input_ids, token_type_ids: window_type_ids, offsets: window_offsets, }); - if windows.len() > self.max_windows_per_payload { - return Err(DetectionError::PayloadLimit); - } if end == ids.len() { break; } } } + // Keep strings as separate model sequences, but charge the configured + // budget by the padded token volume that inference actually processes. + if padded_token_volume(&windows, self.inference_batch_size) > max_model_tokens { + return Err(DetectionError::PayloadLimit); + } Ok(windows) } @@ -445,6 +453,20 @@ impl RampartDetector { } } +fn padded_token_volume(windows: &[Window], max_batch_size: usize) -> usize { + inference_batches(windows, max_batch_size) + .iter() + .map(|batch| { + batch.len() + * batch + .iter() + .map(|index| windows[*index].input_ids.len()) + .max() + .unwrap_or_default() + }) + .sum() +} + fn validate_model_inputs(model: &InferenceModel) -> PluginResult<()> { let names = model .input_outlets() @@ -704,6 +726,38 @@ mod tests { })); } + #[test] + fn compute_budget_charges_padded_tokens_instead_of_string_count() { + let short_windows = (0..100) + .map(|text_index| Window { + text_index, + input_ids: vec![101, 1, 102], + token_type_ids: vec![0; 3], + offsets: vec![None, Some((0, 1)), None], + }) + .collect::>(); + assert_eq!(padded_token_volume(&short_windows, 16), 300); + assert!(padded_token_volume(&short_windows, 16) <= MODEL_MAX_TOKENS); + + let long_windows = [ + Window { + text_index: 0, + input_ids: vec![0; MODEL_MAX_TOKENS], + token_type_ids: vec![0; MODEL_MAX_TOKENS], + offsets: vec![None; MODEL_MAX_TOKENS], + }, + Window { + text_index: 0, + input_ids: vec![0; 156], + token_type_ids: vec![0; 156], + offsets: vec![None; 156], + }, + ]; + assert_eq!(padded_token_volume(&long_windows, 16), 668); + assert!(padded_token_volume(&long_windows, 16) > MODEL_MAX_TOKENS); + assert!(padded_token_volume(&long_windows, 16) <= 2 * MODEL_MAX_TOKENS); + } + #[test] fn overlap_merge_is_deterministic() { let merged = merge_overlapping_spans(vec![ diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index 7458e0e80..fb7f32cad 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -27,6 +27,10 @@ use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::builtin::escape_json_pointer_segment; use crate::overlay::BuiltinCodecName; +use crate::trajectory::{ + CustomMarkPayloadPolicy, is_known_content_bearing_mark, is_trusted_scope_metadata_value, + preserve_analytical_string, preserves_tool_or_function_name, +}; use super::RampartPiiConfig; use super::model::{Detection, DetectionError, RampartDetector}; @@ -61,6 +65,7 @@ pub(super) struct RampartSanitizer { detector: Arc, target_paths: Arc>>, target_path_patterns: Arc>, + trajectory_policy: Option, min_score: f64, excluded_labels: Arc>, replacement: Arc, @@ -96,12 +101,21 @@ struct SelectedText { text: String, } +#[derive(Clone, Copy)] +enum StringSelection { + Configured, + All, + Semantic, + ScopeMetadata, +} + #[derive(Debug, PartialEq, Eq)] enum SanitizeError { Codec, PayloadLimit, } +#[derive(Clone, Copy, PartialEq, Eq)] enum EventField { Data, CategoryProfile, @@ -150,6 +164,20 @@ impl RampartSanitizer { }; let worker_count = inference_worker_count(); let executor = Arc::new(SanitizerExecutor::new(worker_count)?); + let trajectory_policy = config + .preset + .as_deref() + .map(|_| { + CustomMarkPayloadPolicy::parse(&config.custom_mark_payload_policy).ok_or_else( + || { + PluginError::InvalidConfig(format!( + "unsupported custom-mark payload policy '{}'", + config.custom_mark_payload_policy + )) + }, + ) + }) + .transpose()?; Ok(Self { detector, target_paths: Arc::new( @@ -159,6 +187,7 @@ impl RampartSanitizer { .map(compile_json_pointer) .collect(), ), + trajectory_policy, target_path_patterns: Arc::new( config .target_path_patterns @@ -223,50 +252,65 @@ impl RampartSanitizer { } fn sanitize_json(&self, value: Json) -> Result { + self.sanitize_json_with_selection(value, StringSelection::Configured) + } + + fn sanitize_json_with_selection( + &self, + value: Json, + selection: StringSelection, + ) -> Result { Ok(self - .sanitize_json_values(vec![value])? + .sanitize_json_roots(vec![(Vec::new(), value, selection)])? .pop() .expect("single-value sanitization returns one value")) } - fn sanitize_json_values(&self, values: Vec) -> Result, SanitizeError> { - self.sanitize_json_roots( - values - .into_iter() - .map(|value| (Vec::new(), value)) - .collect(), - ) - } - fn sanitize_json_roots( &self, - mut roots: Vec<(Vec, Json)>, + mut roots: Vec<(Vec, Json, StringSelection)>, ) -> Result, SanitizeError> { let mut texts = Vec::new(); let mut total_bytes = 0; - for (path, value) in &roots { + for (path, value, selection) in &roots { let mut path = path.clone(); - self.collect_strings(value, &mut path, &mut texts, &mut total_bytes)?; + self.collect_strings( + value, + &mut path, + *selection, + None, + false, + true, + &mut texts, + &mut total_bytes, + )?; } let sanitized = self.sanitize_texts(texts)?; let mut index = 0; - for (path, value) in &mut roots { + for (path, value, selection) in &mut roots { let mut path = path.clone(); - self.replace_strings(value, &mut path, &sanitized, &mut index); + self.replace_strings( + value, &mut path, *selection, None, false, true, &sanitized, &mut index, + ); } - Ok(roots.into_iter().map(|(_, value)| value).collect()) + Ok(roots.into_iter().map(|(_, value, _)| value).collect()) } + #[allow(clippy::too_many_arguments)] fn collect_strings( &self, value: &Json, path: &mut Vec, + selection: StringSelection, + field: Option<&str>, + preserve: bool, + selection_root: bool, texts: &mut Vec, total_bytes: &mut usize, ) -> Result<(), SanitizeError> { match value { - Json::String(text) if self.matches_path(path) => { + Json::String(text) if self.selects_string(selection, path, field, preserve) => { if texts.len() >= MAX_TEXTS_PER_PAYLOAD { return Err(SanitizeError::PayloadLimit); } @@ -282,15 +326,38 @@ impl RampartSanitizer { Json::Array(items) => { for (index, item) in items.iter().enumerate() { path.push(index.to_string()); - let result = self.collect_strings(item, path, texts, total_bytes); + let result = self.collect_strings( + item, + path, + selection, + field, + false, + false, + texts, + total_bytes, + ); path.pop(); result?; } } Json::Object(fields) => { + let preserve_name = preserves_tool_or_function_name(field, fields); for (key, value) in fields { path.push(escape_json_pointer_segment(key)); - let result = self.collect_strings(value, path, texts, total_bytes); + let preserve = (key == "name" && preserve_name && value.is_string()) + || (matches!(selection, StringSelection::ScopeMetadata) + && selection_root + && is_trusted_scope_metadata_value(key, value)); + let result = self.collect_strings( + value, + path, + selection, + Some(key), + preserve, + false, + texts, + total_bytes, + ); path.pop(); result?; } @@ -300,38 +367,66 @@ impl RampartSanitizer { Ok(()) } - fn has_selected_string(&self, value: &Json) -> bool { - self.has_selected_string_at(value, &mut Vec::new()) + fn has_selected_string_with_selection(&self, value: &Json, selection: StringSelection) -> bool { + self.has_selected_string_at(value, &mut Vec::new(), selection, None, false, true) } - fn has_selected_string_at(&self, value: &Json, path: &mut Vec) -> bool { + fn has_selected_string_at( + &self, + value: &Json, + path: &mut Vec, + selection: StringSelection, + field: Option<&str>, + preserve: bool, + selection_root: bool, + ) -> bool { match value { - Json::String(_) => self.matches_path(path), + Json::String(_) => self.selects_string(selection, path, field, preserve), Json::Array(items) => items.iter().enumerate().any(|(index, item)| { path.push(index.to_string()); - let selected = self.has_selected_string_at(item, path); - path.pop(); - selected - }), - Json::Object(fields) => fields.iter().any(|(key, value)| { - path.push(escape_json_pointer_segment(key)); - let selected = self.has_selected_string_at(value, path); + let selected = + self.has_selected_string_at(item, path, selection, field, false, false); path.pop(); selected }), + Json::Object(fields) => { + let preserve_name = preserves_tool_or_function_name(field, fields); + fields.iter().any(|(key, value)| { + path.push(escape_json_pointer_segment(key)); + let preserve = (key == "name" && preserve_name && value.is_string()) + || (matches!(selection, StringSelection::ScopeMetadata) + && selection_root + && is_trusted_scope_metadata_value(key, value)); + let selected = self.has_selected_string_at( + value, + path, + selection, + Some(key), + preserve, + false, + ); + path.pop(); + selected + }) + } _ => false, } } + #[allow(clippy::too_many_arguments)] fn replace_strings( &self, value: &mut Json, path: &mut Vec, + selection: StringSelection, + field: Option<&str>, + preserve: bool, + selection_root: bool, sanitized: &[String], index: &mut usize, ) { match value { - Json::String(text) if self.matches_path(path) => { + Json::String(text) if self.selects_string(selection, path, field, preserve) => { *text = sanitized .get(*index) .cloned() @@ -341,14 +436,30 @@ impl RampartSanitizer { Json::Array(items) => { for (item_index, item) in items.iter_mut().enumerate() { path.push(item_index.to_string()); - self.replace_strings(item, path, sanitized, index); + self.replace_strings( + item, path, selection, field, false, false, sanitized, index, + ); path.pop(); } } Json::Object(fields) => { + let preserve_name = preserves_tool_or_function_name(field, fields); for (key, value) in fields { path.push(escape_json_pointer_segment(key)); - self.replace_strings(value, path, sanitized, index); + let preserve = (key == "name" && preserve_name && value.is_string()) + || (matches!(selection, StringSelection::ScopeMetadata) + && selection_root + && is_trusted_scope_metadata_value(key, value)); + self.replace_strings( + value, + path, + selection, + Some(key), + preserve, + false, + sanitized, + index, + ); path.pop(); } } @@ -356,6 +467,22 @@ impl RampartSanitizer { } } + fn selects_string( + &self, + selection: StringSelection, + path: &[String], + field: Option<&str>, + preserve: bool, + ) -> bool { + match selection { + StringSelection::Configured => self.matches_path(path), + StringSelection::All => true, + StringSelection::Semantic | StringSelection::ScopeMetadata => { + !preserve && !field.is_some_and(preserve_analytical_string) + } + } + } + fn matches_path(&self, path: &[String]) -> bool { self.target_paths.contains(path) || self @@ -453,8 +580,12 @@ impl RampartSanitizer { ) -> Result { let annotated = codec.decode(request).map_err(|_| SanitizeError::Codec)?; let annotated = serde_json::to_value(annotated).map_err(|_| SanitizeError::Codec)?; - let (headers, annotated) = - self.sanitize_request_parts(request.headers.clone(), annotated)?; + let (headers, annotated) = self.sanitize_request_parts( + request.headers.clone(), + annotated, + StringSelection::Configured, + StringSelection::Configured, + )?; let annotated = serde_json::from_value(annotated).map_err(|_| SanitizeError::Codec)?; let mut encoded = codec .encode(&annotated, request) @@ -466,7 +597,13 @@ impl RampartSanitizer { fn sanitize_raw_request(&self, mut request: LlmRequest) -> Result { let headers = std::mem::take(&mut request.headers); let content = std::mem::take(&mut request.content); - let (headers, content) = self.sanitize_request_parts(headers, content)?; + let (header_selection, content_selection) = if self.trajectory_policy.is_some() { + (StringSelection::All, StringSelection::Semantic) + } else { + (StringSelection::Configured, StringSelection::Configured) + }; + let (headers, content) = + self.sanitize_request_parts(headers, content, header_selection, content_selection)?; request.headers = headers; request.content = content; Ok(request) @@ -476,10 +613,16 @@ impl RampartSanitizer { &self, headers: Map, content: Json, + header_selection: StringSelection, + content_selection: StringSelection, ) -> Result<(Map, Json), SanitizeError> { let mut values = self.sanitize_json_roots(vec![ - (vec!["headers".to_string()], Json::Object(headers)), - (Vec::new(), content), + ( + vec!["headers".to_string()], + Json::Object(headers), + header_selection, + ), + (Vec::new(), content, content_selection), ])?; let content = values.pop().ok_or(SanitizeError::Codec)?; let headers = values.pop().ok_or(SanitizeError::Codec)?; @@ -569,7 +712,12 @@ impl RampartSanitizer { pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeFn { Arc::new(move |_name, payload| { let backend = backend.clone(); - if !backend.has_selected_string(&payload) { + let selection = if backend.trajectory_policy.is_some() { + StringSelection::All + } else { + StringSelection::Configured + }; + if !backend.has_selected_string_with_selection(&payload, selection) { return Box::pin(async move { Ok(payload) }); } let fallback = backend.fail_closed_payload(); @@ -578,7 +726,7 @@ pub(super) fn tool_sanitize_callback(backend: RampartSanitizer) -> ToolSanitizeF return Ok(fallback); }; run_inference("tool payload", permit, fallback, move || { - backend.sanitize_json(payload) + backend.sanitize_json_with_selection(payload, selection) }) .await }) @@ -606,28 +754,33 @@ pub(super) fn event_sanitize_callback( return Ok(fallback); }; run_inference("event fields", permit, fallback, move || { - let specialized_scope = is_specialized_scope(event.as_ref()); - let mut selected = Vec::with_capacity(3); - if !specialized_scope && let Some(data) = fields.data.take() { - selected.push((EventField::Data, data)); + if let Some(selection) = + event_field_selection(&backend, event.as_ref(), EventField::Data) + && let Some(data) = fields.data.take() + { + selected.push((EventField::Data, data, selection)); } - if !specialized_scope + if let Some(selection) = + event_field_selection(&backend, event.as_ref(), EventField::CategoryProfile) && let Some(profile) = fields.category_profile.take() && let Ok(profile) = serde_json::to_value(profile) { - selected.push((EventField::CategoryProfile, profile)); + selected.push((EventField::CategoryProfile, profile, selection)); } - if let Some(metadata) = fields.metadata.take() { - selected.push((EventField::Metadata, metadata)); + if let Some(selection) = + event_field_selection(&backend, event.as_ref(), EventField::Metadata) + && let Some(metadata) = fields.metadata.take() + { + selected.push((EventField::Metadata, metadata, selection)); } - let values = selected + let roots = selected .iter_mut() - .map(|(_, value)| std::mem::take(value)) + .map(|(_, value, selection)| (Vec::new(), std::mem::take(value), *selection)) .collect(); - let sanitized_values = backend.sanitize_json_values(values)?; - for ((field, _), value) in selected.into_iter().zip(sanitized_values) { + let sanitized_values = backend.sanitize_json_roots(roots)?; + for ((field, _, _), value) in selected.into_iter().zip(sanitized_values) { match field { EventField::Data => fields.data = Some(value), EventField::CategoryProfile => { @@ -646,13 +799,23 @@ pub(super) fn event_sanitize_callback( pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSanitizeRequestFn { Arc::new(move |request, context| { let backend = backend.clone(); + if backend.trajectory_policy.is_some() + && !request.headers.values().any(|value| { + backend.has_selected_string_with_selection(value, StringSelection::All) + }) + && !backend + .has_selected_string_with_selection(&request.content, StringSelection::Semantic) + { + return Box::pin(async move { Ok(Some(request)) }); + } Box::pin(async move { let Some(permit) = backend.admit("llm_request").await else { return Ok(None); }; run_inference("LLM request", permit, None, move || { - let sanitized = if matches!(context.codec(), LlmCodecIdentity::None) - && backend.legacy_surface.is_none() + let sanitized = if backend.trajectory_policy.is_some() + || (matches!(context.codec(), LlmCodecIdentity::None) + && backend.legacy_surface.is_none()) { backend.sanitize_raw_request(request).map(Some) } else { @@ -691,11 +854,21 @@ pub(super) fn llm_sanitize_request_callback(backend: RampartSanitizer) -> LlmSan pub(super) fn llm_sanitize_response_callback(backend: RampartSanitizer) -> LlmSanitizeResponseFn { Arc::new(move |payload, context| { let backend = backend.clone(); + if backend.trajectory_policy.is_some() + && !backend.has_selected_string_with_selection(&payload, StringSelection::Semantic) + { + return Box::pin(async move { Ok(Some(payload)) }); + } Box::pin(async move { let Some(permit) = backend.admit("llm_response").await else { return Ok(None); }; run_inference("LLM response", permit, None, move || { + if backend.trajectory_policy.is_some() { + return backend + .sanitize_json_with_selection(payload, StringSelection::Semantic) + .map(Some); + } if matches!(context.codec(), LlmCodecIdentity::None) && backend.legacy_surface.is_none() { @@ -830,25 +1003,57 @@ fn event_fields_have_selected_strings( event: &Event, fields: &EventSanitizeFields, ) -> bool { - if is_specialized_scope(event) { - return fields - .metadata - .as_ref() - .is_some_and(|metadata| backend.has_selected_string(metadata)); - } + let has_selected = |field, value: &Json| { + event_field_selection(backend, event, field) + .is_some_and(|selection| backend.has_selected_string_with_selection(value, selection)) + }; + fields .data .as_ref() - .is_some_and(|data| backend.has_selected_string(data)) - || fields - .category_profile - .as_ref() - .and_then(|profile| serde_json::to_value(profile).ok()) - .is_some_and(|profile| backend.has_selected_string(&profile)) + .is_some_and(|data| has_selected(EventField::Data, data)) + || fields.category_profile.as_ref().is_some_and(|profile| { + serde_json::to_value(profile) + .ok() + .is_some_and(|profile| has_selected(EventField::CategoryProfile, &profile)) + }) || fields .metadata .as_ref() - .is_some_and(|metadata| backend.has_selected_string(metadata)) + .is_some_and(|metadata| has_selected(EventField::Metadata, metadata)) +} + +fn event_field_selection( + backend: &RampartSanitizer, + event: &Event, + field: EventField, +) -> Option { + if backend.trajectory_policy.is_none() { + return (!is_specialized_scope(event) || field == EventField::Metadata) + .then_some(StringSelection::Configured); + } + + let unknown_custom_mark = matches!(event, Event::Mark(_)) + && event + .category() + .is_some_and(|category| category.as_str() == "custom") + && !is_known_content_bearing_mark(event.name()); + if unknown_custom_mark { + return (backend.trajectory_policy == Some(CustomMarkPayloadPolicy::RedactAllLeaves)) + .then_some(StringSelection::All); + } + + if is_specialized_scope(event) { + return (field == EventField::Metadata).then_some(StringSelection::ScopeMetadata); + } + + Some( + if field == EventField::Metadata && matches!(event, Event::Scope(_)) { + StringSelection::ScopeMetadata + } else { + StringSelection::Semantic + }, + ) } fn is_specialized_scope(event: &Event) -> bool { @@ -976,6 +1181,177 @@ mod tests { .unwrap() } + fn trajectory_sanitizer( + detector: Arc, + custom_mark_payload_policy: &str, + ) -> RampartSanitizer { + RampartSanitizer::new( + RampartPiiConfig { + model_path: "/tmp/rampart".into(), + preset: Some("trajectory_context".into()), + custom_mark_payload_policy: custom_mark_payload_policy.into(), + ..RampartPiiConfig::default() + }, + detector, + ) + .unwrap() + } + + #[tokio::test(flavor = "current_thread")] + async fn trajectory_preset_sanitizes_multi_message_anthropic_request_without_projection() { + use nemo_relay::api::runtime::LlmSanitizeRequestContext; + + let backend = trajectory_sanitizer(Arc::new(NameDetector), "preserve"); + let request = LlmRequest { + headers: Map::from_iter([( + "x-user-context".into(), + Json::String("José header".into()), + )]), + content: serde_json::json!({ + "model": "claude-José", + "system": "Help José safely", + "messages": [ + { + "role": "user", + "content": [{"type": "text", "text": "Initial prompt from José"}] + }, + { + "role": "assistant", + "content": [{ + "type": "tool_use", + "id": "toolu_José", + "name": "read_file", + "input": {"path": "/tmp/José.txt"} + }] + }, + { + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "toolu_José", + "content": "The file belongs to José" + }] + } + ], + "tools": [{ + "name": "read_file", + "description": "Read files for José", + "input_schema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "José's file path"} + } + } + }] + }), + }; + + let sanitized = llm_sanitize_request_callback(backend)( + request, + LlmSanitizeRequestContext::with_identity(LlmCodecIdentity::BuiltIn( + BuiltinLlmCodec::AnthropicMessages, + )), + ) + .await + .unwrap() + .expect("trajectory content should remain observable after sanitization"); + + assert_eq!(sanitized.headers["x-user-context"], "[REDACTED] header"); + assert_eq!(sanitized.content["model"], "claude-José"); + assert_eq!(sanitized.content["system"], "Help [REDACTED] safely"); + assert_eq!( + sanitized.content["messages"][0]["content"][0]["text"], + "Initial prompt from [REDACTED]" + ); + assert_eq!( + sanitized.content["messages"][1]["content"][0]["name"], + "read_file" + ); + assert_eq!( + sanitized.content["messages"][1]["content"][0]["id"], + "toolu_José" + ); + assert_eq!( + sanitized.content["messages"][1]["content"][0]["input"]["path"], + "/tmp/[REDACTED].txt" + ); + assert_eq!( + sanitized.content["messages"][2]["content"][0]["tool_use_id"], + "toolu_José" + ); + assert_eq!( + sanitized.content["messages"][2]["content"][0]["content"], + "The file belongs to [REDACTED]" + ); + assert_eq!(sanitized.content["tools"][0]["name"], "read_file"); + assert_eq!( + sanitized.content["tools"][0]["description"], + "Read files for [REDACTED]" + ); + } + + #[tokio::test(flavor = "current_thread")] + async fn trajectory_preset_sanitizes_root_scalar_tool_results() { + let output = tool_sanitize_callback(trajectory_sanitizer( + Arc::new(NameDetector), + "preserve", + ))("read_file".into(), Json::String("Owned by José".into())) + .await + .unwrap(); + + assert_eq!(output, "Owned by [REDACTED]"); + } + + #[tokio::test(flavor = "current_thread")] + async fn trajectory_preset_preserves_unknown_custom_marks_by_default() { + use nemo_relay::api::event::{BaseEvent, EventCategory, MarkEvent}; + + let calls = Arc::new(AtomicUsize::new(0)); + let event = Arc::new(Event::Mark(MarkEvent::new( + BaseEvent::builder() + .name("application.checkpoint") + .data(serde_json::json!({"message": "José"})) + .metadata(serde_json::json!({"owner": "José"})) + .build(), + Some(EventCategory::custom()), + None, + ))); + let fields = event.sanitize_fields(); + let output = event_sanitize_callback( + trajectory_sanitizer(Arc::new(CountingDetector(Arc::clone(&calls))), "preserve"), + None, + )(Arc::clone(&event), fields.clone()) + .await + .unwrap(); + + assert_eq!(output, fields); + assert_eq!(calls.load(Ordering::Relaxed), 0); + } + + #[tokio::test(flavor = "current_thread")] + async fn trajectory_preset_can_inspect_all_unknown_custom_mark_strings() { + use nemo_relay::api::event::{BaseEvent, EventCategory, MarkEvent}; + + let event = Arc::new(Event::Mark(MarkEvent::new( + BaseEvent::builder() + .name("application.checkpoint") + .data(serde_json::json!({"id": "José"})) + .metadata(serde_json::json!({"owner": "José"})) + .build(), + Some(EventCategory::custom()), + None, + ))); + let output = event_sanitize_callback( + trajectory_sanitizer(Arc::new(NameDetector), "redact_all_leaves"), + None, + )(Arc::clone(&event), event.sanitize_fields()) + .await + .unwrap(); + + assert_eq!(output.data.unwrap()["id"], "[REDACTED]"); + assert_eq!(output.metadata.unwrap()["owner"], "[REDACTED]"); + } + #[test] fn sanitizes_selected_utf8_spans_without_touching_metadata() { let sanitizer = sanitizer( diff --git a/crates/pii-redaction/src/trajectory.rs b/crates/pii-redaction/src/trajectory.rs index 4b509ccdf..d413d8515 100644 --- a/crates/pii-redaction/src/trajectory.rs +++ b/crates/pii-redaction/src/trajectory.rs @@ -188,7 +188,7 @@ fn sanitize_scope_metadata(value: Json, replacement: &str) -> Json { ) } -fn is_trusted_scope_metadata_value(key: &str, value: &Json) -> bool { +pub(super) fn is_trusted_scope_metadata_value(key: &str, value: &Json) -> bool { match value { Json::String(_) => TRUSTED_STRING_SCOPE_METADATA_FIELDS.contains(&key), Json::Bool(_) => TRUSTED_BOOLEAN_SCOPE_METADATA_FIELDS.contains(&key), @@ -196,7 +196,7 @@ fn is_trusted_scope_metadata_value(key: &str, value: &Json) -> bool { } } -fn is_known_content_bearing_mark(name: &str) -> bool { +pub(super) fn is_known_content_bearing_mark(name: &str) -> bool { matches!( name, "llm.chunk" | "nemo_relay.llm.optimization" | "skill.load" @@ -433,7 +433,7 @@ fn preserve_analytical_number(key: &str) -> bool { || key.ends_with("_timestamp") } -fn preserve_analytical_string(key: &str) -> bool { +pub(super) fn preserve_analytical_string(key: &str) -> bool { if matches!(key, "token" | "token_id") { return false; } @@ -494,7 +494,7 @@ fn preserve_analytical_string(key: &str) -> bool { || key.ends_with("_uuid") } -fn preserves_tool_or_function_name( +pub(super) fn preserves_tool_or_function_name( container: Option<&str>, object: &serde_json::Map, ) -> bool { diff --git a/go/nemo_relay/pii_rampart.go b/go/nemo_relay/pii_rampart.go index 220779b66..497ac3284 100644 --- a/go/nemo_relay/pii_rampart.go +++ b/go/nemo_relay/pii_rampart.go @@ -14,23 +14,25 @@ const RampartModelRevision = "b1993e4e68b082835b80ffc65acc03325ea2e501" // RampartPiiConfig configures in-process Rampart PII redaction. type RampartPiiConfig struct { - Version uint32 `json:"version"` - ModelPath string `json:"model_path"` - Input bool `json:"input"` - Output bool `json:"output"` - Mark bool `json:"mark"` - ToolInput bool `json:"tool_input"` - ToolOutput bool `json:"tool_output"` - Priority int32 `json:"priority"` - Codec string `json:"codec,omitempty"` - TargetPaths []string `json:"target_paths,omitempty"` - TargetPathPatterns []string `json:"target_path_patterns,omitempty"` - MinScore float64 `json:"min_score"` - ExcludedLabels []string `json:"excluded_labels,omitempty"` - Replacement string `json:"replacement"` - MaxWindowsPerPayload int32 `json:"max_windows_per_payload"` - InferenceBatchSize int32 `json:"inference_batch_size"` - Policy *ConfigPolicy `json:"policy,omitempty"` + Version uint32 `json:"version"` + ModelPath string `json:"model_path"` + Input bool `json:"input"` + Output bool `json:"output"` + Mark bool `json:"mark"` + ToolInput bool `json:"tool_input"` + ToolOutput bool `json:"tool_output"` + Priority int32 `json:"priority"` + Codec string `json:"codec,omitempty"` + Preset string `json:"preset,omitempty"` + TargetPaths []string `json:"target_paths,omitempty"` + TargetPathPatterns []string `json:"target_path_patterns,omitempty"` + MinScore float64 `json:"min_score"` + ExcludedLabels []string `json:"excluded_labels,omitempty"` + Replacement string `json:"replacement"` + CustomMarkPayloadPolicy string `json:"custom_mark_payload_policy"` + MaxWindowsPerPayload int32 `json:"max_windows_per_payload"` + InferenceBatchSize int32 `json:"inference_batch_size"` + Policy *ConfigPolicy `json:"policy,omitempty"` } // RampartPiiComponentSpec wraps one Rampart PII config as a top-level plugin component. @@ -40,24 +42,25 @@ type RampartPiiComponentSpec struct { } // NewRampartPiiConfig returns Rampart PII settings with runtime defaults. -// Set TargetPaths or TargetPathPatterns before validation or activation. +// Set Preset, TargetPaths, or TargetPathPatterns before validation or activation. func NewRampartPiiConfig(modelPath string) RampartPiiConfig { return RampartPiiConfig{ - Version: 1, - ModelPath: modelPath, - Input: true, - Output: true, - Mark: true, - ToolInput: true, - ToolOutput: true, - Priority: 100, - TargetPaths: []string{}, - TargetPathPatterns: []string{}, - MinScore: 0.4, - ExcludedLabels: []string{}, - Replacement: "[REDACTED]", - MaxWindowsPerPayload: 4, - InferenceBatchSize: 16, + Version: 1, + ModelPath: modelPath, + Input: true, + Output: true, + Mark: true, + ToolInput: true, + ToolOutput: true, + Priority: 100, + TargetPaths: []string{}, + TargetPathPatterns: []string{}, + MinScore: 0.4, + ExcludedLabels: []string{}, + Replacement: "[REDACTED]", + CustomMarkPayloadPolicy: "preserve", + MaxWindowsPerPayload: 4, + InferenceBatchSize: 16, } } diff --git a/go/nemo_relay/pii_rampart/pii_rampart.go b/go/nemo_relay/pii_rampart/pii_rampart.go index cbb50e273..b3938e074 100644 --- a/go/nemo_relay/pii_rampart/pii_rampart.go +++ b/go/nemo_relay/pii_rampart/pii_rampart.go @@ -20,7 +20,7 @@ const ModelID = nemo_relay.RampartModelID const ModelRevision = nemo_relay.RampartModelRevision // NewConfig returns Rampart PII settings with runtime defaults. -// Set TargetPaths or TargetPathPatterns before validation or activation. +// Set Preset, TargetPaths, or TargetPathPatterns before validation or activation. func NewConfig(modelPath string) Config { return nemo_relay.NewRampartPiiConfig(modelPath) } diff --git a/go/nemo_relay/pii_rampart/pii_rampart_test.go b/go/nemo_relay/pii_rampart/pii_rampart_test.go index a361ffd99..56c50d8d0 100644 --- a/go/nemo_relay/pii_rampart/pii_rampart_test.go +++ b/go/nemo_relay/pii_rampart/pii_rampart_test.go @@ -59,3 +59,20 @@ func TestValidateConfig(t *testing.T) { } t.Fatalf("expected target_path_patterns diagnostic, got %#v", report.Diagnostics) } + +func TestValidateTrajectoryPreset(t *testing.T) { + modelPath, err := filepath.Abs("testdata/rampart") + if err != nil { + t.Fatalf("resolve model path: %v", err) + } + config := NewConfig(modelPath) + config.Preset = "trajectory_context" + + report, err := ValidateConfig(config) + if err != nil { + t.Fatalf("ValidateConfig failed: %v", err) + } + if len(report.Diagnostics) != 0 { + t.Fatalf("unexpected diagnostics: %#v", report.Diagnostics) + } +} diff --git a/go/nemo_relay/pii_rampart_test.go b/go/nemo_relay/pii_rampart_test.go index a3ff1a5b5..17513db4d 100644 --- a/go/nemo_relay/pii_rampart_test.go +++ b/go/nemo_relay/pii_rampart_test.go @@ -22,6 +22,9 @@ func TestRampartPiiConfigHelpers(t *testing.T) { if config.MaxWindowsPerPayload != 4 { t.Fatalf("unexpected Rampart PII window limit: %d", config.MaxWindowsPerPayload) } + if config.CustomMarkPayloadPolicy != "preserve" { + t.Fatalf("unexpected Rampart PII custom mark policy: %s", config.CustomMarkPayloadPolicy) + } if RampartModelID != "nationaldesignstudio/rampart" || RampartModelRevision != "b1993e4e68b082835b80ffc65acc03325ea2e501" { t.Fatalf("unexpected Rampart model identity: %s@%s", RampartModelID, RampartModelRevision) @@ -34,6 +37,23 @@ func TestRampartPiiConfigHelpers(t *testing.T) { } } +func TestValidateRampartPiiTrajectoryPreset(t *testing.T) { + modelPath, err := filepath.Abs("testdata/rampart") + if err != nil { + t.Fatalf("resolve model path: %v", err) + } + config := NewRampartPiiConfig(modelPath) + config.Preset = "trajectory_context" + + report, err := ValidateRampartPiiConfig(config) + if err != nil { + t.Fatalf("ValidateRampartPiiConfig failed: %v", err) + } + if len(report.Diagnostics) != 0 { + t.Fatalf("unexpected diagnostics: %#v", report.Diagnostics) + } +} + func TestRampartPiiConfigPreservesExplicitZeroValues(t *testing.T) { config := NewRampartPiiConfig("/models/rampart") config.Version = 0 diff --git a/python/nemo_relay/pii_rampart.py b/python/nemo_relay/pii_rampart.py index c2a51272c..f03a42922 100644 --- a/python/nemo_relay/pii_rampart.py +++ b/python/nemo_relay/pii_rampart.py @@ -32,11 +32,13 @@ class RampartPiiConfig: tool_output: bool = True priority: int = 100 codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = None + preset: Literal["trajectory_context"] | str | None = None target_paths: list[str] = field(default_factory=list) target_path_patterns: list[str] = field(default_factory=list) min_score: float = 0.4 excluded_labels: list[str] = field(default_factory=list) replacement: str = "[REDACTED]" + custom_mark_payload_policy: Literal["preserve", "redact_all_leaves"] | str = "preserve" max_windows_per_payload: int = 4 inference_batch_size: int = 16 policy: ConfigPolicy = field(default_factory=ConfigPolicy) @@ -57,12 +59,15 @@ def to_dict(self) -> JsonObject: "min_score": self.min_score, "excluded_labels": self.excluded_labels, "replacement": self.replacement, + "custom_mark_payload_policy": self.custom_mark_payload_policy, "max_windows_per_payload": self.max_windows_per_payload, "inference_batch_size": self.inference_batch_size, "policy": self.policy.to_dict(), } if self.codec is not None: value["codec"] = self.codec + if self.preset is not None: + value["preset"] = self.preset return value diff --git a/python/nemo_relay/pii_rampart.pyi b/python/nemo_relay/pii_rampart.pyi index a2755f8a9..b4fca1741 100644 --- a/python/nemo_relay/pii_rampart.pyi +++ b/python/nemo_relay/pii_rampart.pyi @@ -23,11 +23,13 @@ class RampartPiiConfig: tool_output: bool = ... priority: int = ... codec: Literal["openai_chat", "openai_responses", "anthropic_messages"] | str | None = ... + preset: Literal["trajectory_context"] | str | None = ... target_paths: list[str] = field(default_factory=list) target_path_patterns: list[str] = field(default_factory=list) min_score: float = ... excluded_labels: list[str] = field(default_factory=list) replacement: str = ... + custom_mark_payload_policy: Literal["preserve", "redact_all_leaves"] | str = ... max_windows_per_payload: int = ... inference_batch_size: int = ... policy: ConfigPolicy = field(default_factory=ConfigPolicy) diff --git a/python/tests/test_pii_rampart_plugin.py b/python/tests/test_pii_rampart_plugin.py index e8bbabe30..ec8d70cd0 100644 --- a/python/tests/test_pii_rampart_plugin.py +++ b/python/tests/test_pii_rampart_plugin.py @@ -22,6 +22,7 @@ def test_rampart_config_and_component_shape() -> None: assert value["model_path"] == "/models/rampart" assert value["max_windows_per_payload"] == 4 assert value["inference_batch_size"] == 16 + assert value["custom_mark_payload_policy"] == "preserve" assert RAMPART_MODEL_ID == "nationaldesignstudio/rampart" assert RAMPART_MODEL_REVISION == "b1993e4e68b082835b80ffc65acc03325ea2e501" component = ComponentSpec(config).to_dict() @@ -29,6 +30,16 @@ def test_rampart_config_and_component_shape() -> None: assert component["enabled"] is True +def test_rampart_trajectory_preset_shape_and_validation() -> None: + config = RampartPiiConfig( + model_path="/models/rampart", + preset="trajectory_context", + ) + + assert config.to_dict()["preset"] == "trajectory_context" + assert validate_config(config)["diagnostics"] == [] + + def test_rampart_validation_and_discovery() -> None: report = validate_config( RampartPiiConfig( From be1725e4ed7093e08200be78ce1ffcebc759c66d Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Mon, 3 Aug 2026 17:17:26 -0700 Subject: [PATCH 83/83] fix(pii): preserve repeated agent context Signed-off-by: Alex Fournier --- crates/pii-redaction/src/rampart/sanitizer.rs | 578 +++++++++++++----- 1 file changed, 441 insertions(+), 137 deletions(-) diff --git a/crates/pii-redaction/src/rampart/sanitizer.rs b/crates/pii-redaction/src/rampart/sanitizer.rs index fb7f32cad..196367aae 100644 --- a/crates/pii-redaction/src/rampart/sanitizer.rs +++ b/crates/pii-redaction/src/rampart/sanitizer.rs @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use std::collections::HashSet; +use std::collections::{HashMap, HashSet, VecDeque}; use std::panic::{AssertUnwindSafe, catch_unwind}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::Duration; use nemo_relay::api::event::{Event, EventSanitizeFields}; @@ -23,6 +23,7 @@ use rayon::{ThreadPool, ThreadPoolBuilder}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::{Map, Value as Json}; +use sha2::{Digest, Sha256}; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::builtin::escape_json_pointer_segment; @@ -37,6 +38,8 @@ use super::model::{Detection, DetectionError, RampartDetector}; const MAX_TEXTS_PER_PAYLOAD: usize = 256; const MAX_PAYLOAD_TEXT_BYTES: usize = 256 * 1024; +const MAX_CACHE_ENTRIES: usize = 4096; +const MAX_CACHE_DECISION_BYTES: usize = 4 * 1024 * 1024; // Cap CPU workers while respecting smaller hosts and container CPU quotas. const MAX_CONCURRENT_INFERENCE: usize = 3; // Bound admitted work and its wait so large payloads cannot build a long queue. @@ -73,6 +76,7 @@ pub(super) struct RampartSanitizer { admission_capacity: Arc, execution_admission: Arc, executor: Arc, + cache: Arc>, } #[derive(Clone)] @@ -97,8 +101,119 @@ impl JsonPointerPattern { } } -struct SelectedText { - text: String, +enum SelectedText { + Resolved(String), + Pending { + key: TextCacheKey, + text: Option, + }, +} + +type TextCacheKey = [u8; 32]; + +// Cache decisions rather than text so selected observability content is not retained. +#[derive(Clone)] +enum SanitizationDecision { + Keep, + Redact(Arc<[(usize, usize)]>), + FailClosed, +} + +impl SanitizationDecision { + fn apply(&self, text: &str, replacement: &str) -> String { + match self { + Self::Keep => text.to_string(), + Self::FailClosed => replacement.to_string(), + Self::Redact(ranges) => { + let mut redacted = text.to_string(); + for &(start, end) in ranges.iter().rev() { + if start >= end + || end > redacted.len() + || !redacted.is_char_boundary(start) + || !redacted.is_char_boundary(end) + { + return replacement.to_string(); + } + redacted.replace_range(start..end, replacement); + } + redacted + } + } + } + + fn cache_weight(&self) -> usize { + match self { + Self::Keep | Self::FailClosed => 1, + Self::Redact(ranges) => ranges.len() * std::mem::size_of::<(usize, usize)>(), + } + } +} + +struct CacheEntry { + decision: SanitizationDecision, + referenced: bool, + weight: usize, +} + +#[derive(Default)] +struct SanitizationCache { + entries: HashMap, + order: VecDeque, + decision_bytes: usize, +} + +impl SanitizationCache { + fn get(&mut self, key: &TextCacheKey) -> Option { + let entry = self.entries.get_mut(key)?; + entry.referenced = true; + Some(entry.decision.clone()) + } + + fn insert(&mut self, key: TextCacheKey, decision: SanitizationDecision) { + let weight = decision.cache_weight(); + if weight > MAX_CACHE_DECISION_BYTES { + return; + } + if let Some(previous) = self.entries.remove(&key) { + self.decision_bytes = self.decision_bytes.saturating_sub(previous.weight); + self.order.retain(|existing| existing != &key); + } + self.decision_bytes += weight; + self.entries.insert( + key, + CacheEntry { + decision, + referenced: false, + weight, + }, + ); + self.order.push_back(key); + self.evict(); + } + + fn evict(&mut self) { + while self.entries.len() > MAX_CACHE_ENTRIES + || self.decision_bytes > MAX_CACHE_DECISION_BYTES + { + let Some(key) = self.order.pop_front() else { + break; + }; + let Some(entry) = self.entries.get_mut(&key) else { + continue; + }; + if entry.referenced { + entry.referenced = false; + self.order.push_back(key); + continue; + } + let entry = self.entries.remove(&key).expect("cache entry should exist"); + self.decision_bytes = self.decision_bytes.saturating_sub(entry.weight); + } + } +} + +fn text_cache_key(text: &str) -> TextCacheKey { + Sha256::digest(text.as_bytes()).into() } #[derive(Clone, Copy)] @@ -112,7 +227,6 @@ enum StringSelection { #[derive(Debug, PartialEq, Eq)] enum SanitizeError { Codec, - PayloadLimit, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -202,6 +316,7 @@ impl RampartSanitizer { admission_capacity: Arc::new(Semaphore::new(MAX_ADMITTED_INFERENCE)), execution_admission: Arc::new(Semaphore::new(worker_count)), executor, + cache: Arc::new(Mutex::new(SanitizationCache::default())), }) } @@ -272,6 +387,8 @@ impl RampartSanitizer { ) -> Result, SanitizeError> { let mut texts = Vec::new(); let mut total_bytes = 0; + let mut pending_keys = HashSet::new(); + let mut rejected_fields = 0; for (path, value, selection) in &roots { let mut path = path.clone(); self.collect_strings( @@ -283,10 +400,24 @@ impl RampartSanitizer { true, &mut texts, &mut total_bytes, + &mut pending_keys, + &mut rejected_fields, )?; } - let sanitized = self.sanitize_texts(texts)?; + if rejected_fields > 0 { + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + selected_text_count = pending_keys.len(), + failed_closed_field_count = rejected_fields, + reason = "selection_budget"; + "Rampart PII selected-text budget was exceeded and affected fields failed closed" + ); + } + + let sanitized = self.sanitize_texts(texts); let mut index = 0; for (path, value, selection) in &mut roots { let mut path = path.clone(); @@ -308,20 +439,33 @@ impl RampartSanitizer { selection_root: bool, texts: &mut Vec, total_bytes: &mut usize, + pending_keys: &mut HashSet, + rejected_fields: &mut usize, ) -> Result<(), SanitizeError> { match value { Json::String(text) if self.selects_string(selection, path, field, preserve) => { - if texts.len() >= MAX_TEXTS_PER_PAYLOAD { - return Err(SanitizeError::PayloadLimit); - } - let Some(next_total) = total_bytes.checked_add(text.len()) else { - return Err(SanitizeError::PayloadLimit); - }; - if next_total > MAX_PAYLOAD_TEXT_BYTES { - return Err(SanitizeError::PayloadLimit); + let key = text_cache_key(text); + if let Some(decision) = self.cached_decision(&key) { + texts.push(SelectedText::Resolved( + decision.apply(text, self.replacement.as_ref()), + )); + } else if pending_keys.contains(&key) { + texts.push(SelectedText::Pending { key, text: None }); + } else if pending_keys.len() < MAX_TEXTS_PER_PAYLOAD + && total_bytes + .checked_add(text.len()) + .is_some_and(|next_total| next_total <= MAX_PAYLOAD_TEXT_BYTES) + { + *total_bytes += text.len(); + pending_keys.insert(key); + texts.push(SelectedText::Pending { + key, + text: Some(text.clone()), + }); + } else { + *rejected_fields += 1; + texts.push(SelectedText::Resolved(self.replacement.to_string())); } - *total_bytes = next_total; - texts.push(SelectedText { text: text.clone() }); } Json::Array(items) => { for (index, item) in items.iter().enumerate() { @@ -335,6 +479,8 @@ impl RampartSanitizer { false, texts, total_bytes, + pending_keys, + rejected_fields, ); path.pop(); result?; @@ -357,6 +503,8 @@ impl RampartSanitizer { false, texts, total_bytes, + pending_keys, + rejected_fields, ); path.pop(); result?; @@ -491,38 +639,116 @@ impl RampartSanitizer { .any(|pattern| pattern.matches(path)) } - fn sanitize_texts(&self, mut texts: Vec) -> Result, SanitizeError> { - if !texts.is_empty() { - let selected_text_count = texts.len(); - match self.sanitize_batch(&mut texts) { - Ok(()) => {} + fn cached_decision(&self, key: &TextCacheKey) -> Option { + self.cache.lock().ok()?.get(key) + } + + fn cache_decision(&self, key: TextCacheKey, decision: SanitizationDecision) { + if let Ok(mut cache) = self.cache.lock() { + cache.insert(key, decision); + } + } + + fn sanitize_texts(&self, texts: Vec) -> Vec { + let pending = texts + .iter() + .filter_map(|selected| match selected { + SelectedText::Pending { + key, + text: Some(text), + } => Some((*key, text.clone())), + SelectedText::Resolved(_) | SelectedText::Pending { text: None, .. } => None, + }) + .collect::>(); + let decisions = self.sanitize_pending_texts(&pending); + let rendered = pending + .iter() + .map(|(key, text)| { + let decision = decisions + .get(key) + .cloned() + .unwrap_or(SanitizationDecision::FailClosed); + (*key, decision.apply(text, self.replacement.as_ref())) + }) + .collect::>(); + + texts + .into_iter() + .map(|selected| match selected { + SelectedText::Resolved(text) => text, + SelectedText::Pending { key, .. } => rendered + .get(&key) + .cloned() + .unwrap_or_else(|| self.replacement.to_string()), + }) + .collect() + } + + fn sanitize_pending_texts( + &self, + pending: &[(TextCacheKey, String)], + ) -> HashMap { + let mut decisions = HashMap::new(); + let mut groups = vec![(0..pending.len()).collect::>()]; + while let Some(mut group) = groups.pop() { + if group.is_empty() { + continue; + } + let texts = group + .iter() + .map(|index| pending[*index].1.as_str()) + .collect::>(); + match self.detect_decisions(&texts) { + Ok(group_decisions) => { + for (index, decision) in group.into_iter().zip(group_decisions) { + let key = pending[index].0; + self.cache_decision(key, decision.clone()); + decisions.insert(key, decision); + } + } + Err(DetectionError::PayloadLimit) if group.len() > 1 => { + let right = group.split_off(group.len() / 2); + groups.push(right); + groups.push(group); + } Err(DetectionError::PayloadLimit) => { - return Err(SanitizeError::PayloadLimit); + let index = group[0]; + let key = pending[index].0; + let decision = SanitizationDecision::FailClosed; + self.cache_decision(key, decision.clone()); + decisions.insert(key, decision); + log::warn!( + target: "nemo_relay.plugin", + event = "rampart_pii_inference_failed", + plugin_kind = super::RAMPART_PII_PLUGIN_KIND, + selected_text_bytes = pending[index].1.len(), + reason = "field_payload_limit"; + "Rampart PII field exceeded its model budget and failed closed" + ); } Err(DetectionError::Model(_)) => { log::warn!( target: "nemo_relay.plugin", event = "rampart_pii_inference_failed", plugin_kind = super::RAMPART_PII_PLUGIN_KIND, - selected_text_count, + selected_text_count = group.len(), reason = "model_or_output"; "Rampart PII inference failed closed" ); - for selected in &mut texts { - selected.text = self.replacement.to_string(); + for index in group { + decisions.insert(pending[index].0, SanitizationDecision::FailClosed); } } } } - Ok(texts.into_iter().map(|selected| selected.text).collect()) + decisions } - fn sanitize_batch(&self, texts: &mut [SelectedText]) -> Result<(), DetectionError> { - let selected = texts - .iter() - .map(|selected| selected.text.as_str()) - .collect::>(); - let detections = self.detector.detect(&selected)?; + fn detect_decisions( + &self, + texts: &[&str], + ) -> Result, DetectionError> { + let detections = self.detector.detect(texts)?; let mut by_text = vec![Vec::::new(); texts.len()]; for detection in detections { if detection.text_index >= texts.len() @@ -536,41 +762,42 @@ impl RampartSanitizer { by_text[detection.text_index].push(detection); } - for (selected, mut detections) in texts.iter_mut().zip(by_text) { - detections.retain(|detection| { - detection.score >= self.min_score - && !self.excluded_labels.contains(&detection.label) - }); - if detections.is_empty() { - continue; - } - detections.sort_by_key(|detection| (detection.start_utf8, detection.end_utf8)); - let text = &selected.text; - let mut previous_end = 0; - for detection in &detections { - if detection.start_utf8 >= detection.end_utf8 - || detection.end_utf8 > text.len() - || !text.is_char_boundary(detection.start_utf8) - || !text.is_char_boundary(detection.end_utf8) - || detection.start_utf8 < previous_end - { - return Err(PluginError::Internal( - "Rampart returned invalid or overlapping UTF-8 spans".into(), - ) - .into()); + texts + .iter() + .zip(by_text) + .map(|(text, mut detections)| { + detections.retain(|detection| { + detection.score >= self.min_score + && !self.excluded_labels.contains(&detection.label) + }); + if detections.is_empty() { + return Ok(SanitizationDecision::Keep); } - previous_end = detection.end_utf8; - } - let mut redacted = text.clone(); - for detection in detections.iter().rev() { - redacted.replace_range( - detection.start_utf8..detection.end_utf8, - self.replacement.as_ref(), - ); - } - selected.text = redacted; - } - Ok(()) + detections.sort_by_key(|detection| (detection.start_utf8, detection.end_utf8)); + let mut previous_end = 0; + for detection in &detections { + if detection.start_utf8 >= detection.end_utf8 + || detection.end_utf8 > text.len() + || !text.is_char_boundary(detection.start_utf8) + || !text.is_char_boundary(detection.end_utf8) + || detection.start_utf8 < previous_end + { + return Err(PluginError::Internal( + "Rampart returned invalid or overlapping UTF-8 spans".into(), + ) + .into()); + } + previous_end = detection.end_utf8; + } + Ok(SanitizationDecision::Redact( + detections + .into_iter() + .map(|detection| (detection.start_utf8, detection.end_utf8)) + .collect::>() + .into(), + )) + }) + .collect() } fn sanitize_request_with_codec( @@ -930,17 +1157,6 @@ where }); match receiver.await { Ok(Ok(Ok(value))) => Ok(value), - Ok(Ok(Err(SanitizeError::PayloadLimit))) => { - log::warn!( - target: "nemo_relay.plugin", - event = "rampart_pii_inference_failed", - plugin_kind = super::RAMPART_PII_PLUGIN_KIND, - reason = "payload_limit", - target; - "Rampart PII sanitization exceeded a payload limit and failed closed" - ); - Ok(fallback) - } Ok(Ok(Err(SanitizeError::Codec))) => Ok(fallback), Ok(Err(_)) => { log::error!( @@ -1137,6 +1353,28 @@ mod tests { } } + struct CountingNameDetector(Arc); + + impl DetectionModel for CountingNameDetector { + fn detect(&self, texts: &[&str]) -> Result, DetectionError> { + self.0.fetch_add(1, Ordering::Relaxed); + NameDetector.detect(texts) + } + } + + struct BatchLimitedNameDetector(Arc); + + impl DetectionModel for BatchLimitedNameDetector { + fn detect(&self, texts: &[&str]) -> Result, DetectionError> { + self.0.fetch_add(1, Ordering::Relaxed); + if texts.len() > 1 { + Err(DetectionError::PayloadLimit) + } else { + NameDetector.detect(texts) + } + } + } + struct BlockingDetector { started: Arc, release: Arc, @@ -1373,6 +1611,64 @@ mod tests { ); } + #[test] + fn content_cache_deduplicates_within_and_across_payloads() { + let calls = Arc::new(AtomicUsize::new(0)); + let sanitizer = sanitizer( + Arc::new(CountingNameDetector(Arc::clone(&calls))), + vec!["/*"], + ); + let payload = serde_json::json!({ + "first": "Hello José", + "second": "Hello José" + }); + + let first = sanitizer.sanitize_json(payload.clone()).unwrap(); + let second = sanitizer.sanitize_json(payload).unwrap(); + + assert_eq!(first["first"], "Hello [REDACTED]"); + assert_eq!(first["second"], "Hello [REDACTED]"); + assert_eq!(second, first); + assert_eq!(calls.load(Ordering::Relaxed), 1); + } + + #[test] + fn content_cache_evicts_to_its_entry_bound() { + let mut cache = SanitizationCache::default(); + for index in 0..=MAX_CACHE_ENTRIES { + cache.insert( + text_cache_key(&index.to_string()), + SanitizationDecision::Keep, + ); + } + + assert_eq!(cache.entries.len(), MAX_CACHE_ENTRIES); + assert_eq!(cache.order.len(), MAX_CACHE_ENTRIES); + assert_eq!(cache.decision_bytes, MAX_CACHE_ENTRIES); + } + + #[test] + fn payload_limited_batch_splits_without_dropping_the_envelope() { + let calls = Arc::new(AtomicUsize::new(0)); + let sanitizer = sanitizer( + Arc::new(BatchLimitedNameDetector(Arc::clone(&calls))), + vec!["/*"], + ); + + let sanitized = sanitizer + .sanitize_json(serde_json::json!({ + "first": "José one", + "second": "José two", + "metadata": 7 + })) + .unwrap(); + + assert_eq!(sanitized["first"], "[REDACTED] one"); + assert_eq!(sanitized["second"], "[REDACTED] two"); + assert_eq!(sanitized["metadata"], 7); + assert_eq!(calls.load(Ordering::Relaxed), 3); + } + #[test] fn exact_selectors_match_escaped_json_pointer_segments() { let sanitizer = RampartSanitizer::new( @@ -1433,17 +1729,29 @@ mod tests { } #[test] - fn selected_text_count_limit_rejects_the_entire_payload() { - let sanitizer = sanitizer(Arc::new(NameDetector), vec!["/*"]); + fn selected_text_count_limit_redacts_only_excess_unique_fields() { + let calls = Arc::new(AtomicUsize::new(0)); + let sanitizer = sanitizer(Arc::new(CountingDetector(Arc::clone(&calls))), vec!["/*"]); let value = Json::Object( (0..=MAX_TEXTS_PER_PAYLOAD) - .map(|index| (index.to_string(), Json::String("safe".into()))) + .map(|index| { + ( + index.to_string(), + Json::String(format!("safe-value-{index}")), + ) + }) .collect(), ); - assert_eq!( - sanitizer.sanitize_json(value), - Err(SanitizeError::PayloadLimit) - ); + let sanitized = sanitizer.sanitize_json(value).unwrap(); + let redacted = sanitized + .as_object() + .unwrap() + .values() + .filter(|value| value.as_str() == Some("[REDACTED]")) + .count(); + + assert_eq!(redacted, 1); + assert_eq!(calls.load(Ordering::Relaxed), 1); } #[test] @@ -1464,16 +1772,18 @@ mod tests { assert_eq!(calls.load(Ordering::Relaxed), 1); assert_eq!( - sanitizer.sanitize_json(serde_json::json!({ - "message": " ".repeat(MAX_PAYLOAD_TEXT_BYTES + 1) - })), - Err(SanitizeError::PayloadLimit) + sanitizer + .sanitize_json(serde_json::json!({ + "message": " ".repeat(MAX_PAYLOAD_TEXT_BYTES + 1) + })) + .unwrap()["message"], + "[REDACTED]" ); assert_eq!(calls.load(Ordering::Relaxed), 1); } #[tokio::test(flavor = "current_thread")] - async fn aggregate_limit_fails_closed_before_partial_tool_sanitization() { + async fn aggregate_limit_redacts_only_the_field_beyond_the_budget() { let calls = Arc::new(AtomicUsize::new(0)); let backend = sanitizer( Arc::new(CountingDetector(Arc::clone(&calls))), @@ -1492,8 +1802,10 @@ mod tests { .await .unwrap(); - assert_eq!(output, Json::String("[REDACTED]".into())); - assert_eq!(calls.load(Ordering::Relaxed), 0); + assert_eq!(output["messages"][0]["content"], "first-private-value"); + assert_eq!(output["messages"][1]["content"], "[REDACTED]"); + assert_eq!(output["metadata"], "visible"); + assert_eq!(calls.load(Ordering::Relaxed), 1); } #[test] @@ -1614,7 +1926,7 @@ mod tests { serde_json::json!({"message": "private"}) ); } - assert_eq!(started.load(Ordering::Acquire), MAX_ADMITTED_INFERENCE); + assert_eq!(started.load(Ordering::Acquire), worker_count); }); } @@ -1726,21 +2038,20 @@ mod tests { } #[tokio::test(flavor = "current_thread")] - async fn model_window_limit_fails_closed_for_every_surface() { + async fn model_window_limit_fails_closed_only_for_affected_fields() { use nemo_relay::api::event::{BaseEvent, MarkEvent}; use nemo_relay::api::runtime::{LlmSanitizeRequestContext, LlmSanitizeResponseContext}; let backend = sanitizer(Arc::new(PayloadLimitedDetector), vec!["/message"]); let private = "must-not-pass-through"; - assert_eq!( - tool_sanitize_callback(backend.clone())( - "tool".into(), - serde_json::json!({"message": private, "metadata": "visible"}), - ) - .await - .unwrap(), - Json::String("[REDACTED]".into()) - ); + let tool = tool_sanitize_callback(backend.clone())( + "tool".into(), + serde_json::json!({"message": private, "metadata": "visible"}), + ) + .await + .unwrap(); + assert_eq!(tool["message"], "[REDACTED]"); + assert_eq!(tool["metadata"], "visible"); let event = Arc::new(Event::Mark(MarkEvent::new( BaseEvent::builder() @@ -1751,38 +2062,35 @@ mod tests { None, None, ))); - assert_eq!( - event_sanitize_callback(backend.clone(), None)( - Arc::clone(&event), - event.sanitize_fields(), - ) - .await - .unwrap(), - EventSanitizeFields::default() - ); + let event_fields = event_sanitize_callback(backend.clone(), None)( + Arc::clone(&event), + event.sanitize_fields(), + ) + .await + .unwrap(); + assert_eq!(event_fields.data.unwrap()["message"], "[REDACTED]"); + assert_eq!(event_fields.metadata.unwrap()["message"], "[REDACTED]"); let request = LlmRequest { headers: Map::new(), content: serde_json::json!({"message": private}), }; - assert!( - llm_sanitize_request_callback(backend.clone())( - request, - LlmSanitizeRequestContext::default(), - ) - .await - .unwrap() - .is_none() - ); - assert!( - llm_sanitize_response_callback(backend.clone())( - serde_json::json!({"message": private}), - LlmSanitizeResponseContext::default(), - ) - .await - .unwrap() - .is_none() - ); + let request = llm_sanitize_request_callback(backend.clone())( + request, + LlmSanitizeRequestContext::default(), + ) + .await + .unwrap() + .expect("field-level failure should preserve the request envelope"); + assert_eq!(request.content["message"], "[REDACTED]"); + let response = llm_sanitize_response_callback(backend.clone())( + serde_json::json!({"message": private}), + LlmSanitizeResponseContext::default(), + ) + .await + .unwrap() + .expect("field-level failure should preserve the response envelope"); + assert_eq!(response["message"], "[REDACTED]"); let codec = build_response_codec(ProviderSurface::OpenAIChat); let payload = serde_json::json!({ @@ -1794,14 +2102,10 @@ mod tests { "finish_reason": "stop" }] }); - assert_eq!( - backend.sanitize_response_with_codec( - codec.as_ref(), - ProviderSurface::OpenAIChat, - payload, - ), - Err(SanitizeError::PayloadLimit) - ); + let sanitized = backend + .sanitize_response_with_codec(codec.as_ref(), ProviderSurface::OpenAIChat, payload) + .unwrap(); + assert_eq!(sanitized["choices"][0]["message"]["content"], "[REDACTED]"); } #[test]