Event Sanitizers

View as Markdown

Event sanitizers let you remove or replace observability data on mark and scope events before the runtime sends the events to subscribers or exporters. Use them for event data that the tool and LLM request or response sanitizer APIs do not cover. They do not change the real request, response, or callback result.

Sanitizer Surfaces

Choose a registry based on the event that you want to sanitize.

RegistryEvents
Mark sanitizerEvery mark, including explicit, pending, plugin-runtime, and streaming marks
Scope-start sanitizerEvery scope category with scope_category = "start"
Scope-end sanitizerEvery scope category with scope_category = "end"

Scope sanitizers apply to every scope category, including tool and llm. For tool and LLM events, the specialized request or response sanitizers run first. Your scope sanitizer then receives the already-sanitized event fields, which lets a general event policy inspect them without changing the real request or response.

Callback Contract

When the runtime calls a sanitizer, it provides the following values:

  • The complete event as immutable context.
  • An EventSanitizeFields value with data, category_profile, and metadata.

Return the replacement values for those three observability fields. The return value replaces the fields; it does not patch them. Start with the supplied fields, modify the fields that need to change, and return all three. In bindings that accept a partial object, omitting a field can clear it. To remove a field explicitly, use None in Rust or Python, null in Node.js, or a zero-value json.RawMessage in Go.

Sanitizers can change only data, category_profile, and metadata. They cannot change event identity, parentage, timestamps, kind, scope category, 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, a synchronous sanitizer callback that throws also fails open; Relay records the error for getLastCallbackError().

Registration Lifetimes

Where you register a sanitizer determines how long it stays active.

  • Global registrations stay active until you explicitly deregister them.
  • Scope-local registrations are inherited by descendant scopes and are removed when the owning scope closes.
  • Plugin-context registrations belong to the component instance. The runtime rolls them back if activation fails or you clear the plugin configuration.

Register a Global Mark Sanitizer

The following examples replace mark data and remove mark metadata while preserving the category profile.

1import nemo_relay
2from nemo_relay import guardrails
3
4def sanitize_mark(event, fields: nemo_relay.EventSanitizeFields):
5 return {
6 "data": {"checkpoint": event.name},
7 "category_profile": fields["category_profile"],
8 "metadata": None,
9 }
10
11guardrails.register_mark_sanitize("safe-marks", 100, sanitize_mark)
12try:
13 nemo_relay.scope.event("planning", data={"secret": "value"})
14finally:
15 guardrails.deregister_mark_sanitize("safe-marks")

API Names

Use the matching start and end APIs when a policy applies to duration-bearing scope events. The following APIs register sanitizers at each level.

LevelPythonNode.jsRust
Global Markguardrails.register_mark_sanitizeregisterMarkSanitizeGuardrailregister_mark_sanitize_guardrail
Global Startguardrails.register_scope_sanitize_startregisterScopeSanitizeStartGuardrailregister_scope_sanitize_start_guardrail
Global Endguardrails.register_scope_sanitize_endregisterScopeSanitizeEndGuardrailregister_scope_sanitize_end_guardrail
Scope-Local Markscope_local.register_mark_sanitizescopeRegisterMarkSanitizeGuardrailscope_register_mark_sanitize_guardrail
Scope-Local Startscope_local.register_scope_sanitize_startscopeRegisterScopeSanitizeStartGuardrailscope_register_scope_sanitize_start_guardrail
Scope-Local Endscope_local.register_scope_sanitize_endscopeRegisterScopeSanitizeEndGuardrailscope_register_scope_sanitize_end_guardrail

Each registration API has a matching deregistration API. Python scope-local helpers accept a scope handle. Node.js and Rust scope-local helpers accept the owning scope UUID.

Plugin contexts expose the same three surfaces as register_mark_sanitize_guardrail, register_scope_sanitize_start_guardrail, and register_scope_sanitize_end_guardrail, using each binding’s naming convention.

Dynamic Plugins

Rust native plugins receive the three registration methods on their typed plugin context. Python grpc-v1 workers receive the same methods on PluginContext, and their callbacks can be synchronous or asynchronous. The worker protocol remains grpc-v1 and defines the following stable registration surface values:

  • MARK_SANITIZE_GUARDRAIL = 30
  • SCOPE_SANITIZE_START_GUARDRAIL = 31
  • SCOPE_SANITIZE_END_GUARDRAIL = 32

Declare only the surfaces that your dynamic plugin installs. Register them through the plugin context so the runtime rolls back every registration if 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.

For related concepts and configuration, refer to the following topics: