Skip to content

feat(guard): llm-guard I/O security for agent input & output#17

Open
ngoclam9415 wants to merge 2 commits into
developfrom
feature/llm-guard-io-security
Open

feat(guard): llm-guard I/O security for agent input & output#17
ngoclam9415 wants to merge 2 commits into
developfrom
feature/llm-guard-io-security

Conversation

@ngoclam9415

Copy link
Copy Markdown
Contributor

Summary

Integrate protectai/llm-guard as a pluggable GuardService that secures STARAgent input & output at the single query/aquery choke point.

Behavior

  • Input: block-listed scanners (default prompt_injection) → request refused (STAR loop skipped, refusal + attempt recorded). Secrets/PII findings → sanitize + pass.
  • Output: gated two-stage scrub — rule scanners strip/redact first; the LLM scrub fires only when a scanner flags (clean output skips the LLM call). Scrubbed text overwrites the persisted AGENT_RESPONSE so raw sensitive data never lands in timeline.json or replays to the LLM next turn.
  • Pluggable like llm_provider: scanner registry + STARAgent(guard_instance=...) injection.
  • Fail-open: missing/broken llm-guard models → no-op + warn, never breaks a run. Thread-safe lazy scanner init.
  • Audit: structlog events + timeline.json metadata.

Architecture

New dana/core/guard/: GuardService Protocol (3 methods) + LLMGuardService, NoOpGuardService, OutputSanitizer, scanner registry, env-driven GuardConfig, result types. Wired into STARAgent.

Config (env)

DANA_GUARD_ENABLED, _INPUT_SCANNERS, _OUTPUT_SCANNERS, _SANITIZE_LLM_ENABLED, _FAIL_MODE, _BLOCK_ON, _BLOCK_MESSAGE.

Dependency note ⚠️

llm-guard added as a core dependency (per design decision). Its constraints downgrade transformers 5.3.0→4.51.3, tokenizers, regex, pyopenssl and add spacy. It pulls torch/transformers (~GB) — uv sync not yet run in this branch; guard is fail-open so runtime works without it until synced.

Tests

  • 32 new guard unit tests (no torch needed — stubbed llm-guard + fake LLM).
  • Full core unit suite green. Guard disabled by default in tests/conftest.py.
  • 3 pre-existing test_llm_providers.py failures are unrelated (fail on clean base).

Review

Adversarial code review run; fixed CRITICAL (raw response persisted to timeline → leak/replay), thread-safety, scanner-registry clobbering, wrong-entry annotation.

Test plan

  • uv sync to install llm-guard, then exercise with a real provider
  • Verify injection input is refused and PII output is scrubbed end-to-end
  • Confirm transformers downgrade doesn't regress other paths

https://claude.ai/code/session_01WdeJ8nFuNdE7yuLKBcZx37

Integrate protectai/llm-guard as a pluggable GuardService securing
STARAgent input and output at the query/aquery choke point.

- Input: block-listed scanners (default prompt_injection) refuse the
  request; secrets/PII findings sanitize + pass.
- Output: gated two-stage scrub - rule scanners strip/redact, LLM scrub
  fires only when a scanner flags. Scrubbed text overwrites the persisted
  AGENT_RESPONSE so raw data never lands on disk or replays to the LLM.
- Pluggable scanner registry (mirrors llm_provider); injectable via
  STARAgent(guard_instance=...). Fail-open: missing/broken models no-op +
  warn. Thread-safe lazy scanner init.
- Audit to structlog + timeline.json metadata.
- Config via DANA_GUARD_* env vars. llm-guard added as core dependency
  (downgrades transformers->4.51.3 per its constraints); guard disabled
  by default in tests.

Tests: 32 guard unit tests; full core suite green.

Claude-Session: https://claude.ai/code/session_01WdeJ8nFuNdE7yuLKBcZx37
llm-guard keys results by scanner CLASS name (PromptInjection), but
block_on / findings / config use snake_case (prompt_injection), so the
input block branch was dead for every default scanner — detected
injections downgraded to SANITIZED and reached the LLM unchanged.

- scanner_factory: build_* now also return a class-name -> snake_case
  registry-key map (joined via type(scanner).__name__).
- LLMGuardService: translate llm-guard's class-name keys to snake_case
  when building findings/triggered, so they match config out of the box.
- Block matching normalized (case/underscore-insensitive) so block_on
  also tolerates the class-name spelling.
- Tests: pin the default-config injection -> BLOCKED path with the real
  class-name vocabulary (the prior stubs used snake_case and masked the
  bug); add name-map + unmapped-key coverage.

Claude-Session: https://claude.ai/code/session_01WdeJ8nFuNdE7yuLKBcZx37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant