NVIDIA / NVIDIA/NeMo-Agent-Toolkit

Defense middlewares: PII / content safety / output verifier analyze structured stream chunks as pydantic reprs

Open
#2,235 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
2.6k
Forks
762
Avg merge
21h 28m
Merged PRs (30d)
27

Description

Version

1.9.0 (nvidia-nat-security==1.9.0), also present on develop at 013255a.

Which installation method(s) does this occur on?
  • PyPi
  • Source
Describe the bug.

The three stream-buffering defense middlewares serialize each buffered chunk with str(chunk) before analysis, so typed chunks (for example ChatResponseChunk, which the toolkit's own react_agent yields) are analyzed, length-checked, and anonymized as pydantic repr dumps instead of the generated reply text:

  • OutputVerifierMiddleware (defense_middleware_output_verifier.py:406) sends the repr to the verifier LLM.
  • ContentSafetyGuardMiddleware (defense_middleware_content_guard.py:422) sends the repr to the guard model and counts the repr length against max_content_length. Reprs are far longer than the text, so streams that fit the configured budget can stop early with input length ... exceeds configured max_content_length.
  • PIIDefenseMiddleware (defense_middleware_pii.py:311) runs PII detection/anonymization over the repr.

Today, str() on a streamed chunk produces:

id='echo' choices=[ChatResponseChunkChoice(finish_reason=None, index=0, delta=ChoiceDelta(content='Hello world', role=None, tool_calls=None))] created=datetime.datetime(2026, 9, 19, ..., tzinfo=datetime.timezone.utc) model='unknown-model' object='chat.completion.chunk' ...

Expected: the analysis input is the chunk's text content (Hello world), consistent with how Function.astream(to_type=str) renders streamed output through GlobalTypeConverter.

This is the same defect class as #2230 (guardrails middleware, addressed for that middleware by #2234) in the defense middlewares. A fix with one regression test per middleware is ready and will be linked here.

Minimum reproducible example
from nat.data_models.api_server import ChatResponseChunk

chunk = ChatResponseChunk.create_streaming_chunk("Hello world", id_="echo")
print(str(chunk))                        # what the middlewares analyze today
print(chunk.choices[0].delta.content)    # what they should analyze

End-to-end, function_middleware_stream with action="refusal" over a stream of
ChatResponseChunks feeds str(chunk) into the analyzer/guard/verifier; the regression
tests in the linked PR drive all three middlewares that way with mocked analyzers.

Relevant log output
call(text="id='echo' choices=[ChatResponseChunkChoice(finish_reason=None, index=0, delta=ChoiceDelta(content='Hello world', role=None, tool_calls=None))] created=datetime.datetime(...) model='unknown-model' object='chat.completion.chunk' ...")
Other/Misc.

Fix plan: one shared helper (stream_chunk_to_text) that returns strings unchanged, renders toolkit data models through GlobalTypeConverter (the same conversion the framework uses for to_type=str), and keeps the previous str() rendering for values without a text representation. Used by all three middlewares, with a regression test per middleware.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the buffering paths in defense_middleware_output_verifier.py:406, defense_middleware_content_guard.py:422, and defense_middleware_pii.py:311, then inspect GlobalTypeConverter and function_middleware_stream. Add regression coverage for all three middlewares using ChatResponseChunk streams, and verify that analysis receives the chunk text rather than its repr while non-text values retain the previous behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.