Skip to content

feat: add guardrails #97

Open
nina-kollman wants to merge 60 commits intomainfrom
nk/guardrails
Open

feat: add guardrails #97
nina-kollman wants to merge 60 commits intomainfrom
nk/guardrails

Conversation

@nina-kollman
Copy link

@nina-kollman nina-kollman commented Feb 10, 2026

Important

This pull request adds a comprehensive guardrails feature to the codebase, including configuration, validation, and extensive testing for safety, validation, and quality checks in LLM gateway traffic.

  • Guardrails Feature:
    • Introduces guardrails to intercept LLM gateway traffic for safety, validation, and quality checks.
    • Configurable via YAML under guardrails with provider-level defaults.
    • Supports pre-call and post-call guards with concurrent execution.
  • Configuration and Validation:
    • Updates GatewayConfig in types.rs to include guardrails.
    • Adds validation for guardrails in config/validation.rs.
    • Supports environment variable substitution in YAML configs.
  • Testing:
    • Extensive tests added in tests/guardrails/ for guardrails functionality.
    • Includes tests for configuration parsing, guard execution, and client interactions.
    • Uses wiremock for mocking evaluator API responses.
  • Miscellaneous:
    • Updates Cargo.toml and Cargo.lock for new dependencies.
    • Adds GUARDRAILS.md for documentation of the guardrails feature.

This description was created by Ellipsis for 99dff3d. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • Guardrails: configurable pre-call/post-call evaluations with block/warn outcomes, many built-in evaluators (PII, secrets, profanity, prompt‑injection, SQL/regex/JSON validators, tone, toxicity, perplexity, uncertainty, etc.), provider support (Traceloop), middleware enforcement, and pipeline-level guard lists.
  • Documentation

    • Added comprehensive Guardrails guide with examples and observability notes.
  • Tests

    • Extensive unit, integration, and end-to-end test suites and fixtures.
  • Chores

    • Updated gitignore and development/test dependencies.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Guardrails subsystem: new guard types/config, evaluator registry, Traceloop provider client, guard execution runner (pre/post phases) with OpenTelemetry spans, middleware and pipeline integration, extensive validation, tests, cassettes, and documentation. Also updates Cargo deps and .gitignore.

Changes

Cohort / File(s) Summary
Core types & config
src/guardrails/types.rs, src/types/mod.rs, src/config/lib.rs
Add Guard/ProviderConfig/GuardrailsConfig, GuardMode/OnFailure, GuardrailClient trait, Guardrails state; add guards to Pipeline and guardrails to GatewayConfig; serde helpers and hashing.
Evaluator types & parsing
src/guardrails/evaluator_types.rs, src/guardrails/parsing.rs
Introduce evaluator slugs, EvaluatorRequest trait, body builders, per-evaluator configs, get_evaluator registry, attach_config helper, and evaluator response parsing; add Prompt/Completion extractor traits.
Providers & HTTP client
src/guardrails/providers/mod.rs, src/guardrails/providers/traceloop.rs
Add traceloop provider, TRACELOOP_PROVIDER constant, create_guardrail_client, and TraceloopClient implementing GuardrailClient with request/response handling and tests.
Runner, setup & spans
src/guardrails/runner.rs, src/guardrails/setup.rs, src/guardrails/span_attributes.rs
Implement concurrent guard execution, GuardrailsRunner (pre/post phases, finalize/blocked responses), header parsing, guard resolution, provider-default inheritance, span attribute constants, and resource builders.
Middleware & integration
src/guardrails/middleware.rs, src/pipelines/pipeline.rs, src/pipelines/otel.rs, src/state.rs
Add GuardrailsLayer middleware, thread guardrail resources into pipeline/router, adapt pipeline endpoints to return Response and accept headers/guardrails, update tracer to root+LLM spans.
Setup, validation & service wiring
src/guardrails/setup.rs, src/config/validation.rs, src/management/services/config_provider_service.rs
Parse/resolve guard headers, build shared resources, validate guardrails YAML (providers, guards, uniqueness, required fields), initialize Pipeline.guards in config provider transformation.
Module exports & docs
src/guardrails/mod.rs, src/guardrails/GUARDRAILS.md, src/lib.rs
Expose guardrails submodules, add comprehensive GUARDRAILS.md documentation, export runner/setup helpers and add pub mod guardrails to crate root.
Tests & fixtures
tests/guardrails/*, tests/cassettes/guardrails/*.json, tests/*, tests/guardrails/helpers.rs
Large new unit/integration/e2e test suites, mock clients/helpers, many JSON cassettes; tests updated to include guards/guardrails fields.
Build & ignore
Cargo.toml, .gitignore
Add dependency thiserror = "2" and dev-deps for opentelemetry/opentelemetry_sdk; add docs/ and .claude/ to .gitignore.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Pipeline as Pipeline
    participant Pre as Pre-Call Guards
    participant LLM as LLM Model
    participant Post as Post-Call Guards
    participant Traceloop as Traceloop

    Client->>Pipeline: HTTP request + headers
    Pipeline->>Pre: extract_prompt(input)
    Pre->>Traceloop: POST /v2/guardrails/execute/{slug}
    Traceloop-->>Pre: EvaluatorResponse
    alt Pre blocked
        Pre->>Client: 403 Forbidden (blocked_response)
    else Pre passed
        Pipeline->>LLM: call model
        LLM-->>Pipeline: completion
        Pipeline->>Post: extract_completion(output)
        Post->>Traceloop: POST /v2/guardrails/execute/{slug}
        Traceloop-->>Post: EvaluatorResponse
        Post->>Pipeline: GuardrailsOutcome (warnings/blocked)
        Pipeline->>Client: Response (+ warning headers)
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hop through code with ears held high,

New guards on prompts that watch and pry.
Traceloop hums as evaluators peep,
Pre and post keep vigil while models sleep.
A carrot for safety — hop, hop, keep!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add guardrails' directly describes the main feature being added—a comprehensive guardrails system for intercepting LLM gateway traffic. While concise, it clearly summarizes the primary change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nk/guardrails

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

🔒 Container Vulnerability Scan (hub-migrations - amd64)

Click to expand results

For OSS Maintainers: VEX Notice
--------------------------------
If you're an OSS maintainer and Trivy has detected vulnerabilities in your project that you believe are not actually exploitable, consider issuing a VEX (Vulnerability Exploitability eXchange) statement.
VEX allows you to communicate the actual status of vulnerabilities in your project, improving security transparency and reducing false positives for your users.
Learn more and start using VEX: https://trivy.dev/v0.65/docs/supply-chain/vex/repo#publishing-vex-documents

To disable this notice, set the TRIVY_DISABLE_VEX_NOTICE environment variable.


hub-migrations:b189a21bc330e93720b7e4e14e48d4b282e07d69-amd64 (debian 13.3)
===========================================================================
Total: 11 (UNKNOWN: 0, LOW: 7, MEDIUM: 3, HIGH: 1, CRITICAL: 0)

┌─────────┬──────────────────┬──────────┬──────────┬─────────────────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
│ Library │  Vulnerability   │ Severity │  Status  │      Installed Version      │ Fixed Version │                            Title                             │
├─────────┼──────────────────┼──────────┼──────────┼─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ libc6   │ CVE-2026-0861    │ HIGH     │ affected │ 2.41-12+deb13u1             │               │ glibc: Integer overflow in memalign leads to heap corruption │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0861                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2025-15281   │ MEDIUM   │          │                             │               │ glibc: wordexp with WRDE_REUSE and WRDE_APPEND may return    │
│         │                  │          │          │                             │               │ uninitialized memory                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2025-15281                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2026-0915    │          │          │                             │               │ glibc: glibc: Information disclosure via zero-valued network │
│         │                  │          │          │                             │               │ query                                                        │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0915                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2010-4756    │ LOW      │          │                             │               │ glibc: glob implementation can cause excessive CPU and       │
│         │                  │          │          │                             │               │ memory consumption due to...                                 │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2010-4756                    │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2018-20796   │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2018-20796                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010022 │          │          │                             │               │ glibc: stack guard protection bypass                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010022                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010023 │          │          │                             │               │ glibc: running ldd on malicious ELF leads to code execution  │
│         │                  │          │          │                             │               │ because of...                                                │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010023                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010024 │          │          │                             │               │ glibc: ASLR bypass using cache of thread stack and heap      │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010024                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010025 │          │          │                             │               │ glibc: information disclosure of heap addresses of           │
│         │                  │          │          │                             │               │ pthread_created thread                                       │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010025                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-9192    │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-9192                    │
├─────────┼──────────────────┼──────────┤          ├─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ zlib1g  │ CVE-2026-27171   │ MEDIUM   │          │ 1:1.3.dfsg+really1.3.1-1+b1 │               │ zlib: zlib: Denial of Service via infinite loop in CRC32     │
│         │                  │          │          │                             │               │ combine functions...                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-27171                   │
└─────────┴──────────────────┴──────────┴──────────┴─────────────────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘

OS Packages (license)
=====================
Total: 29 (UNKNOWN: 0, LOW: 11, MEDIUM: 0, HIGH: 18, CRITICAL: 0)

┌─────────────┬───────────────────┬────────────────┬──────────┐
│   Package   │      License      │ Classification │ Severity │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ base-files  │ GPL-2.0-or-later  │ restricted     │ HIGH     │
├─────────────┤                   │                │          │
│ gcc-14-base │                   │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-only      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Artistic-2.0      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ LGPL-2.0-or-later │ restricted     │ HIGH     │
├─────────────┼───────────────────┤                │          │
│ libc6       │ LGPL-2.1-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-or-later  │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-only      │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-or-later  │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Unicode-DFS-2016  │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ BSL-1.0           │                │          │
│             ├───────────────────┤                │          │
│             │ BSD-2-Clause      │                │          │
│             ├───────────────────┤                │          │
│             │ ISC               │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-3.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.1-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-only     │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libssl3t64  │ Apache-2.0        │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ Artistic-2.0      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-1.0-or-later  │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ GPL-1.0-only      │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libzstd1    │ BSD-3-Clause      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-2.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Zlib              │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ MIT               │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ netbase     │ GPL-2.0-only      │ restricted     │ HIGH     │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ zlib1g      │ Zlib              │ notice         │ LOW      │
└─────────────┴───────────────────┴────────────────┴──────────┘

@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

🔒 Container Vulnerability Scan (hub-migrations - arm64)

Click to expand results

For OSS Maintainers: VEX Notice
--------------------------------
If you're an OSS maintainer and Trivy has detected vulnerabilities in your project that you believe are not actually exploitable, consider issuing a VEX (Vulnerability Exploitability eXchange) statement.
VEX allows you to communicate the actual status of vulnerabilities in your project, improving security transparency and reducing false positives for your users.
Learn more and start using VEX: https://trivy.dev/v0.65/docs/supply-chain/vex/repo#publishing-vex-documents

To disable this notice, set the TRIVY_DISABLE_VEX_NOTICE environment variable.


hub-migrations:b189a21bc330e93720b7e4e14e48d4b282e07d69-arm64 (debian 13.3)
===========================================================================
Total: 11 (UNKNOWN: 0, LOW: 7, MEDIUM: 3, HIGH: 1, CRITICAL: 0)

┌─────────┬──────────────────┬──────────┬──────────┬─────────────────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
│ Library │  Vulnerability   │ Severity │  Status  │      Installed Version      │ Fixed Version │                            Title                             │
├─────────┼──────────────────┼──────────┼──────────┼─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ libc6   │ CVE-2026-0861    │ HIGH     │ affected │ 2.41-12+deb13u1             │               │ glibc: Integer overflow in memalign leads to heap corruption │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0861                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2025-15281   │ MEDIUM   │          │                             │               │ glibc: wordexp with WRDE_REUSE and WRDE_APPEND may return    │
│         │                  │          │          │                             │               │ uninitialized memory                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2025-15281                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2026-0915    │          │          │                             │               │ glibc: glibc: Information disclosure via zero-valued network │
│         │                  │          │          │                             │               │ query                                                        │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0915                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2010-4756    │ LOW      │          │                             │               │ glibc: glob implementation can cause excessive CPU and       │
│         │                  │          │          │                             │               │ memory consumption due to...                                 │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2010-4756                    │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2018-20796   │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2018-20796                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010022 │          │          │                             │               │ glibc: stack guard protection bypass                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010022                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010023 │          │          │                             │               │ glibc: running ldd on malicious ELF leads to code execution  │
│         │                  │          │          │                             │               │ because of...                                                │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010023                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010024 │          │          │                             │               │ glibc: ASLR bypass using cache of thread stack and heap      │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010024                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010025 │          │          │                             │               │ glibc: information disclosure of heap addresses of           │
│         │                  │          │          │                             │               │ pthread_created thread                                       │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010025                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-9192    │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-9192                    │
├─────────┼──────────────────┼──────────┤          ├─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ zlib1g  │ CVE-2026-27171   │ MEDIUM   │          │ 1:1.3.dfsg+really1.3.1-1+b1 │               │ zlib: zlib: Denial of Service via infinite loop in CRC32     │
│         │                  │          │          │                             │               │ combine functions...                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-27171                   │
└─────────┴──────────────────┴──────────┴──────────┴─────────────────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘

OS Packages (license)
=====================
Total: 29 (UNKNOWN: 0, LOW: 11, MEDIUM: 0, HIGH: 18, CRITICAL: 0)

┌─────────────┬───────────────────┬────────────────┬──────────┐
│   Package   │      License      │ Classification │ Severity │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ base-files  │ GPL-2.0-or-later  │ restricted     │ HIGH     │
├─────────────┤                   │                │          │
│ gcc-14-base │                   │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-only      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Artistic-2.0      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ LGPL-2.0-or-later │ restricted     │ HIGH     │
├─────────────┼───────────────────┤                │          │
│ libc6       │ LGPL-2.1-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-or-later  │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-only      │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-or-later  │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Unicode-DFS-2016  │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ BSL-1.0           │                │          │
│             ├───────────────────┤                │          │
│             │ BSD-2-Clause      │                │          │
│             ├───────────────────┤                │          │
│             │ ISC               │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-3.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.1-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-only     │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libssl3t64  │ Apache-2.0        │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ Artistic-2.0      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-1.0-or-later  │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ GPL-1.0-only      │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libzstd1    │ BSD-3-Clause      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-2.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Zlib              │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ MIT               │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ netbase     │ GPL-2.0-only      │ restricted     │ HIGH     │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ zlib1g      │ Zlib              │ notice         │ LOW      │
└─────────────┴───────────────────┴────────────────┴──────────┘

@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

🔒 Container Vulnerability Scan (hub - amd64)

Click to expand results

For OSS Maintainers: VEX Notice
--------------------------------
If you're an OSS maintainer and Trivy has detected vulnerabilities in your project that you believe are not actually exploitable, consider issuing a VEX (Vulnerability Exploitability eXchange) statement.
VEX allows you to communicate the actual status of vulnerabilities in your project, improving security transparency and reducing false positives for your users.
Learn more and start using VEX: https://trivy.dev/v0.65/docs/supply-chain/vex/repo#publishing-vex-documents

To disable this notice, set the TRIVY_DISABLE_VEX_NOTICE environment variable.


hub:b189a21bc330e93720b7e4e14e48d4b282e07d69-amd64 (debian 13.3)
================================================================
Total: 11 (UNKNOWN: 0, LOW: 7, MEDIUM: 3, HIGH: 1, CRITICAL: 0)

┌─────────┬──────────────────┬──────────┬──────────┬─────────────────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
│ Library │  Vulnerability   │ Severity │  Status  │      Installed Version      │ Fixed Version │                            Title                             │
├─────────┼──────────────────┼──────────┼──────────┼─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ libc6   │ CVE-2026-0861    │ HIGH     │ affected │ 2.41-12+deb13u1             │               │ glibc: Integer overflow in memalign leads to heap corruption │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0861                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2025-15281   │ MEDIUM   │          │                             │               │ glibc: wordexp with WRDE_REUSE and WRDE_APPEND may return    │
│         │                  │          │          │                             │               │ uninitialized memory                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2025-15281                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2026-0915    │          │          │                             │               │ glibc: glibc: Information disclosure via zero-valued network │
│         │                  │          │          │                             │               │ query                                                        │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0915                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2010-4756    │ LOW      │          │                             │               │ glibc: glob implementation can cause excessive CPU and       │
│         │                  │          │          │                             │               │ memory consumption due to...                                 │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2010-4756                    │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2018-20796   │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2018-20796                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010022 │          │          │                             │               │ glibc: stack guard protection bypass                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010022                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010023 │          │          │                             │               │ glibc: running ldd on malicious ELF leads to code execution  │
│         │                  │          │          │                             │               │ because of...                                                │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010023                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010024 │          │          │                             │               │ glibc: ASLR bypass using cache of thread stack and heap      │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010024                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010025 │          │          │                             │               │ glibc: information disclosure of heap addresses of           │
│         │                  │          │          │                             │               │ pthread_created thread                                       │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010025                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-9192    │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-9192                    │
├─────────┼──────────────────┼──────────┤          ├─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ zlib1g  │ CVE-2026-27171   │ MEDIUM   │          │ 1:1.3.dfsg+really1.3.1-1+b1 │               │ zlib: zlib: Denial of Service via infinite loop in CRC32     │
│         │                  │          │          │                             │               │ combine functions...                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-27171                   │
└─────────┴──────────────────┴──────────┴──────────┴─────────────────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘

OS Packages (license)
=====================
Total: 29 (UNKNOWN: 0, LOW: 11, MEDIUM: 0, HIGH: 18, CRITICAL: 0)

┌─────────────┬───────────────────┬────────────────┬──────────┐
│   Package   │      License      │ Classification │ Severity │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ base-files  │ GPL-2.0-or-later  │ restricted     │ HIGH     │
├─────────────┤                   │                │          │
│ gcc-14-base │                   │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-only      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Artistic-2.0      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ LGPL-2.0-or-later │ restricted     │ HIGH     │
├─────────────┼───────────────────┤                │          │
│ libc6       │ LGPL-2.1-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-or-later  │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-only      │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-or-later  │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Unicode-DFS-2016  │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ BSL-1.0           │                │          │
│             ├───────────────────┤                │          │
│             │ BSD-2-Clause      │                │          │
│             ├───────────────────┤                │          │
│             │ ISC               │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-3.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.1-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-only     │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libssl3t64  │ Apache-2.0        │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ Artistic-2.0      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-1.0-or-later  │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ GPL-1.0-only      │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libzstd1    │ BSD-3-Clause      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-2.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Zlib              │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ MIT               │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ netbase     │ GPL-2.0-only      │ restricted     │ HIGH     │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ zlib1g      │ Zlib              │ notice         │ LOW      │
└─────────────┴───────────────────┴────────────────┴──────────┘

@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

🔒 Container Vulnerability Scan (hub - arm64)

Click to expand results

For OSS Maintainers: VEX Notice
--------------------------------
If you're an OSS maintainer and Trivy has detected vulnerabilities in your project that you believe are not actually exploitable, consider issuing a VEX (Vulnerability Exploitability eXchange) statement.
VEX allows you to communicate the actual status of vulnerabilities in your project, improving security transparency and reducing false positives for your users.
Learn more and start using VEX: https://trivy.dev/v0.65/docs/supply-chain/vex/repo#publishing-vex-documents

To disable this notice, set the TRIVY_DISABLE_VEX_NOTICE environment variable.


hub:b189a21bc330e93720b7e4e14e48d4b282e07d69-arm64 (debian 13.3)
================================================================
Total: 11 (UNKNOWN: 0, LOW: 7, MEDIUM: 3, HIGH: 1, CRITICAL: 0)

┌─────────┬──────────────────┬──────────┬──────────┬─────────────────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
│ Library │  Vulnerability   │ Severity │  Status  │      Installed Version      │ Fixed Version │                            Title                             │
├─────────┼──────────────────┼──────────┼──────────┼─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ libc6   │ CVE-2026-0861    │ HIGH     │ affected │ 2.41-12+deb13u1             │               │ glibc: Integer overflow in memalign leads to heap corruption │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0861                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2025-15281   │ MEDIUM   │          │                             │               │ glibc: wordexp with WRDE_REUSE and WRDE_APPEND may return    │
│         │                  │          │          │                             │               │ uninitialized memory                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2025-15281                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2026-0915    │          │          │                             │               │ glibc: glibc: Information disclosure via zero-valued network │
│         │                  │          │          │                             │               │ query                                                        │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-0915                    │
│         ├──────────────────┼──────────┤          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2010-4756    │ LOW      │          │                             │               │ glibc: glob implementation can cause excessive CPU and       │
│         │                  │          │          │                             │               │ memory consumption due to...                                 │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2010-4756                    │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2018-20796   │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2018-20796                   │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010022 │          │          │                             │               │ glibc: stack guard protection bypass                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010022                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010023 │          │          │                             │               │ glibc: running ldd on malicious ELF leads to code execution  │
│         │                  │          │          │                             │               │ because of...                                                │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010023                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010024 │          │          │                             │               │ glibc: ASLR bypass using cache of thread stack and heap      │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010024                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-1010025 │          │          │                             │               │ glibc: information disclosure of heap addresses of           │
│         │                  │          │          │                             │               │ pthread_created thread                                       │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-1010025                 │
│         ├──────────────────┤          │          │                             ├───────────────┼──────────────────────────────────────────────────────────────┤
│         │ CVE-2019-9192    │          │          │                             │               │ glibc: uncontrolled recursion in function                    │
│         │                  │          │          │                             │               │ check_dst_limits_calc_pos_1 in posix/regexec.c               │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2019-9192                    │
├─────────┼──────────────────┼──────────┤          ├─────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ zlib1g  │ CVE-2026-27171   │ MEDIUM   │          │ 1:1.3.dfsg+really1.3.1-1+b1 │               │ zlib: zlib: Denial of Service via infinite loop in CRC32     │
│         │                  │          │          │                             │               │ combine functions...                                         │
│         │                  │          │          │                             │               │ https://avd.aquasec.com/nvd/cve-2026-27171                   │
└─────────┴──────────────────┴──────────┴──────────┴─────────────────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘

OS Packages (license)
=====================
Total: 29 (UNKNOWN: 0, LOW: 11, MEDIUM: 0, HIGH: 18, CRITICAL: 0)

┌─────────────┬───────────────────┬────────────────┬──────────┐
│   Package   │      License      │ Classification │ Severity │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ base-files  │ GPL-2.0-or-later  │ restricted     │ HIGH     │
├─────────────┤                   │                │          │
│ gcc-14-base │                   │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-only      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Artistic-2.0      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ LGPL-2.0-or-later │ restricted     │ HIGH     │
├─────────────┼───────────────────┤                │          │
│ libc6       │ LGPL-2.1-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-or-later │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-or-later  │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-2.0-only      │                │          │
│             ├───────────────────┤                │          │
│             │ GPL-3.0-or-later  │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Unicode-DFS-2016  │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ BSL-1.0           │                │          │
│             ├───────────────────┤                │          │
│             │ BSD-2-Clause      │                │          │
│             ├───────────────────┤                │          │
│             │ ISC               │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-3.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ LGPL-2.0-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-2.1-only     │                │          │
│             ├───────────────────┤                │          │
│             │ LGPL-3.0-only     │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libssl3t64  │ Apache-2.0        │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ Artistic-2.0      │                │          │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-1.0-or-later  │ restricted     │ HIGH     │
│             ├───────────────────┤                │          │
│             │ GPL-1.0-only      │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ libzstd1    │ BSD-3-Clause      │ notice         │ LOW      │
│             ├───────────────────┼────────────────┼──────────┤
│             │ GPL-2.0-only      │ restricted     │ HIGH     │
│             ├───────────────────┼────────────────┼──────────┤
│             │ Zlib              │ notice         │ LOW      │
│             ├───────────────────┤                │          │
│             │ MIT               │                │          │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ netbase     │ GPL-2.0-only      │ restricted     │ HIGH     │
├─────────────┼───────────────────┼────────────────┼──────────┤
│ zlib1g      │ Zlib              │ notice         │ LOW      │
└─────────────┴───────────────────┴────────────────┴──────────┘

@ellipsis-dev ellipsis-dev bot changed the title feat: Add guardrails support feat: add guardrails system for pre-call and post-call request/response validation Feb 11, 2026
@nina-kollman nina-kollman changed the title feat: add guardrails system for pre-call and post-call request/response validation feat: add guardrails Feb 11, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
config-example.yaml (1)

132-163: Make required explicit for the post-call guard to avoid ambiguity.

This is an example config; adding required clarifies expected behavior and avoids relying on defaults.

✅ Suggested tweak
     - name: toxicity-filter
       provider: traceloop
       evaluator_slug: toxicity-detector
       params:
         threshold: 0.8
       mode: post_call  # Runs after the model call
       on_failure: block
+      required: true  # set to false for best-effort behavior
src/guardrails/GUARDRAILS.md (1)

10-11: Improve grammar and sentence structure.

Line 11 is a sentence fragment lacking a proper subject-verb structure. Consider revising for clarity:

✏️ Suggested revision
-Guardrails can be implemented in **Config Mode (Hub v1)** :
-Guardrails fully defined in YAML configuration, applied automatically to gateway requests
+**Config Mode (Hub v1)**: Guardrails are fully defined in YAML configuration and applied automatically to gateway requests.

@kbpranay
Copy link

The introduction of guardrails at the gateway level is a vital step for LLM observability and safety. The variety of evaluators you are adding (PII, secrets, etc.) is quite comprehensive.

I am currently working with OpenClaw (https://clawoncloud.com), which is a personal AI assistant framework designed for self-hosting. We approach safety through a similar mindset of intercepting and validating tool calls and message flows. It is great to see more tooling emerging in this space to make agentic systems more predictable and secure!

@doronkopit5
Copy link
Member

Additional review notes


Architecture: Guardrails should be a Tower layer, not inline handler code

The original pipeline had a clean plugin architecture (PluginConfig::Tracing, PluginConfig::ModelRouter). Guardrails break that pattern by being manually interleaved into the handler body at 3 separate points (setup, pre-call, post-call) rather than being a composable layer.

This is the root cause of issues #1 and #2 from the previous comment — when guardrails are manually stitched into the handler, it's easy to miss a code path. The streaming path was missed because the post-call and finalize logic only lives in the NonStream branch.

The idiomatic axum approach would be a Tower middleware layer:

// Guardrails as a wrapping layer — applies to ALL routes uniformly
if let Some(gr) = guardrails {
    router = router.layer(GuardrailsLayer::new(gr));
}

This would:

  • Keep handlers clean (no guardrails knowledge)
  • Cover all routes uniformly (chat, completions, embeddings) without copy-pasting
  • Make the streaming gap impossible — the layer wraps all responses
  • Fit the existing plugin pattern the pipeline was designed around

pub async fn chat_completions(
State(model_registry): State<Arc<ModelRegistry>>,
headers: HeaderMap,
Json(payload): Json<ChatCompletionRequest>,
model_keys: Vec<String>,
guardrails: Option<Arc<Guardrails>>,
) -> Result<Response, StatusCode> {
let mut tracer = OtelTracer::start();
let parent_cx = tracer.parent_context();
let orchestrator = GuardrailsRunner::new(guardrails.as_deref(), &headers, Some(parent_cx));
// Pre-call guardrails
let mut all_warnings = Vec::new();
if let Some(orch) = &orchestrator {
let pre = orch.run_pre_call(&payload).await;
if let Some(resp) = pre.blocked_response {
return Ok(resp);
}
all_warnings = pre.warnings;
}
for model_key in model_keys {
let model = model_registry.get(&model_key).unwrap();
if payload.model == model.model_type {
tracer.start_llm_span("chat", &payload);
tracer.set_vendor(&get_vendor_name(&model.provider.r#type()));
let response = model
.chat_completions(payload.clone())
.await
.inspect_err(|e| {
eprintln!("Chat completion error for model {model_key}: {e:?}");
})?;
if let ChatCompletionResponse::NonStream(completion) = response {
tracer.log_success(&completion);
// Post-call guardrails (non-streaming)
if let Some(orch) = &orchestrator {
let post = orch.run_post_call(&completion).await;
if let Some(resp) = post.blocked_response {
return Ok(resp);
}
all_warnings.extend(post.warnings);
}
return Ok(GuardrailsRunner::finalize_response(
Json(completion).into_response(),
&all_warnings,
));
}
if let ChatCompletionResponse::Stream(stream) = response {
return Ok(Sse::new(trace_and_stream(tracer, stream))
.keep_alive(KeepAlive::default())
.into_response());
}
}
}
tracer.log_error("No matching model found".to_string());
eprintln!("No matching model found for: {}", payload.model);
Err(StatusCode::NOT_FOUND)
}


Lower-confidence issues (not blocking, but worth noting)

  1. Empty API key silently sent as Authorization: Bearer — When api_key is None, the code defaults to empty string and sends a malformed auth header. Should either omit the header or fail at config validation time.

let api_key = guard.api_key.as_deref().unwrap_or("");
let evaluator = get_evaluator(&guard.evaluator_slug).ok_or_else(|| {
GuardrailError::Unavailable(format!(
"Unknown evaluator slug '{}'",
guard.evaluator_slug
))
})?;
let body = evaluator.build_body(input, &guard.params)?;
let response = self
.http_client
.post(&url)
.header("Authorization", format!("Bearer {api_key}"))

  1. unwrap_or_default() on failed HTTP client builder silently drops timeout — If reqwest::Client::builder().build() fails, the fallback default client has no timeout configured. Should propagate the error instead.

pub fn with_timeout(timeout: std::time::Duration) -> Self {
Self {
http_client: reqwest::Client::builder()
.timeout(timeout)
.build()
.unwrap_or_default(),
}
}

  1. OTEL spans not marked as error on model provider failures — When chat_completions/completions/embeddings return errors, inspect_err logs to stderr but the tracer's llm_span and root_span are dropped without error status. This PR refactored spans into a two-span model (root_span + llm_span), making proper error handling more critical. Historical context: commit 0ba4d15 ("fix otel getting stuck") shows span lifecycle has been a problem area.

let response = model
.chat_completions(payload.clone())
.await
.inspect_err(|e| {
eprintln!("Chat completion error for model {model_key}: {e:?}");
})?;

  1. chat_completions uses two if let instead of match — The NonStream/Stream branches should be a match expression so the compiler enforces exhaustiveness if a variant is added later.

if let ChatCompletionResponse::NonStream(completion) = response {
tracer.log_success(&completion);
// Post-call guardrails (non-streaming)
if let Some(orch) = &orchestrator {
let post = orch.run_post_call(&completion).await;
if let Some(resp) = post.blocked_response {
return Ok(resp);
}
all_warnings.extend(post.warnings);
}
return Ok(GuardrailsRunner::finalize_response(
Json(completion).into_response(),
&all_warnings,
));
}
if let ChatCompletionResponse::Stream(stream) = response {
return Ok(Sse::new(trace_and_stream(tracer, stream))
.keep_alive(KeepAlive::default())
.into_response());
}

🤖 Generated with Claude Code

Copy link
Member

@doronkopit5 doronkopit5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Simplification Suggestions

Here are 7 simplification opportunities I identified in this PR. Together they'd reduce ~70 lines with zero behavioral changes. See inline comments for details.

@doronkopit5
Copy link
Member

Code Simplification Suggestions

Here are 7 simplification opportunities identified in this PR. Together they'd reduce ~70 lines with zero behavioral changes.


1. Extract shared content_to_string helper — src/guardrails/parsing.rs

The ChatMessageContent match block is duplicated verbatim in both extract_prompt() (lines 22-29) and extract_completion() (lines 43-49). Extract into a shared helper:

fn content_to_string(content: &ChatMessageContent) -> String {
    match content {
        ChatMessageContent::String(s) => s.clone(),
        ChatMessageContent::Array(parts) => parts
            .iter()
            .filter(|p| p.r#type == "text")
            .map(|p| p.text.as_str())
            .collect::<Vec<_>>()
            .join(" "),
    }
}

Then both impls simplify to calling content_to_string. Risk: None — pure mechanical extraction.


2. Add evaluator_with_config! macro — src/guardrails/evaluator_types.rs

There's already evaluator_with_no_config! for 6 simple evaluators. The 6 evaluators with config (lines 159-223) follow an identical pattern. Add a companion macro to eliminate ~55 lines:

macro_rules! evaluator_with_config {
    ($name:ident, $body_fn:ident, $config:ty, $slug:expr) => {
        pub struct $name;
        impl EvaluatorRequest for $name {
            fn build_body(
                &self,
                input: &str,
                params: &HashMap<String, serde_json::Value>,
            ) -> Result<serde_json::Value, GuardrailError> {
                attach_config::<$config>($body_fn(input), params, $slug)
            }
        }
    };
}

evaluator_with_config!(PiiDetector, text_body, PiiDetectorConfig, PII_DETECTOR);
evaluator_with_config!(PromptInjection, prompt_body, ThresholdConfig, PROMPT_INJECTION);
evaluator_with_config!(SexismDetector, text_body, ThresholdConfig, SEXISM_DETECTOR);
evaluator_with_config!(ToxicityDetector, text_body, ThresholdConfig, TOXICITY_DETECTOR);
evaluator_with_config!(RegexValidator, text_body, RegexValidatorConfig, REGEX_VALIDATOR);
evaluator_with_config!(JsonValidator, text_body, JsonValidatorConfig, JSON_VALIDATOR);

Risk: None — macro expands to identical code. Adding new evaluators becomes a one-liner.


3. Consolidate four guard validation loops into one — src/config/validation.rs

Lines 52-117 iterate over gr_config.guards four separate times (provider refs, api_base/api_key, evaluator slugs, name uniqueness). Merge into a single pass:

let mut seen_guard_names = HashSet::new();
for guard in &gr_config.guards {
    if !gr_config.providers.contains_key(&guard.provider) {
        errors.push(format!("Guard '{}' references non-existent guardrail provider '{}'.", guard.name, guard.provider));
    }

    let has_api_base = guard.api_base.as_ref().is_some_and(|s| !s.is_empty())
        || gr_config.providers.get(&guard.provider).is_some_and(|p| !p.api_base.is_empty());
    let has_api_key = guard.api_key.as_ref().is_some_and(|s| !s.is_empty())
        || gr_config.providers.get(&guard.provider).is_some_and(|p| !p.api_key.is_empty());
    if !has_api_base { errors.push(format!("Guard '{}' has no api_base ...", guard.name)); }
    if !has_api_key { errors.push(format!("Guard '{}' has no api_key ...", guard.name)); }

    if crate::guardrails::evaluator_types::get_evaluator(&guard.evaluator_slug).is_none() {
        errors.push(format!("Guard '{}' has unknown evaluator_slug '{}'.", guard.name, guard.evaluator_slug));
    }

    if !seen_guard_names.insert(&guard.name) {
        errors.push(format!("Duplicate guard name: '{}'.", guard.name));
    }
}

Error messages stay identical. Risk: None.


4. Builder pattern for test guards — tests/guardrails/helpers.rs

The 4 create_test_guard* functions are slight variations of each other. Replace with a single builder:

pub struct TestGuardBuilder { guard: Guard }

impl TestGuardBuilder {
    pub fn new(name: &str, mode: GuardMode) -> Self { /* base guard */ }
    pub fn on_failure(mut self, on_failure: OnFailure) -> Self { self.guard.on_failure = on_failure; self }
    pub fn required(mut self, required: bool) -> Self { self.guard.required = required; self }
    pub fn api_base(mut self, api_base: &str) -> Self { self.guard.api_base = Some(api_base.to_string()); self }
    pub fn evaluator_slug(mut self, slug: &str) -> Self { self.guard.evaluator_slug = slug.to_string(); self }
    pub fn build(self) -> Guard { self.guard }
}

Usage: TestGuardBuilder::new("blocker", GuardMode::PreCall).on_failure(OnFailure::Block).build()

Old functions can be kept as thin wrappers initially. Risk: Low — test-only code.


5. Remove duplicate guard_with_servertests/guardrails/test_e2e.rs

After item 4, the local guard_with_server() function (lines 32-50) becomes unnecessary. Callers switch to the shared builder:

let guard = TestGuardBuilder::new("blocker", GuardMode::PreCall)
    .on_failure(OnFailure::Block)
    .api_base(&eval.uri())
    .evaluator_slug("toxicity-detector")
    .build();

Depends on: Item 4. Risk: Low — test-only code.


6. Log warning on HTTP client builder failure — src/guardrails/providers/traceloop.rs:33

.unwrap_or_default() silently creates a default client if the builder fails. Replace with a match that logs before falling back:

pub fn with_timeout(timeout: std::time::Duration) -> Self {
    let http_client = match reqwest::Client::builder().timeout(timeout).build() {
        Ok(client) => client,
        Err(e) => {
            tracing::warn!(error = %e, "Failed to build guardrail HTTP client with custom timeout, using default");
            reqwest::Client::default()
        }
    };
    Self { http_client }
}

Same fallback behavior, better observability. Risk: Minimal.


7. Trim implementation details — src/guardrails/GUARDRAILS.md

The "Source Layout" (lines 144-158) and "Key Types" (lines 160-168) sections are implementation details that will go stale. Replace with:

## Implementation

See `src/guardrails/mod.rs` for module structure and key type definitions.

The user-facing docs (How It Works, Configuration, Failure Behavior, Supported Evaluators) are great — keep all of that. Risk: None.


What I intentionally did NOT suggest simplifying

Item Reason
types.rs Hash impls Only 2 call sites with different key/value types — a generic helper would add complexity for marginal gain
runner.rs early-exit Only 4 shared lines, methods diverge after that — extraction adds indirection for no clarity gain
setup.rs resolve_guard_defaults 14 lines of clear imperative mutation; a .map() chain would be same length but harder to follow

🤖 Generated with Claude Code

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pipelines/pipeline.rs (1)

133-138: ⚠️ Potential issue | 🟡 Minor

OTEL span not marked as error when provider call fails.

When chat_completions fails (line 138), the error is propagated via ? but the tracer span is not explicitly marked with an error status. Per reviewer feedback, this can cause OTEL tracing issues. Consider calling tracer.log_error() before returning the error.

📊 Proposed fix to mark span error
             let response = model
                 .chat_completions(payload.clone())
                 .await
                 .inspect_err(|e| {
                     eprintln!("Chat completion error for model {model_key}: {e:?}");
+                    tracer.log_error(format!("Chat completion error: {e:?}"));
-                })?;
+                })
+                .map_err(|e| {
+                    e
+                })?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pipelines/pipeline.rs` around lines 133 - 138, The chat_completions error
path doesn't mark the OTEL span as errored; update the inspect_err closure on
the model.chat_completions(payload.clone()).await call to call
tracer.log_error(...) (or the equivalent method on the tracer/span object in
this module) with the error details before the error is propagated, so the span
is explicitly marked as failed when the call returns Err; ensure you reference
the same tracer/span instance used for this pipeline span when calling
tracer.log_error().
🧹 Nitpick comments (4)
src/guardrails/middleware.rs (2)

30-37: Path matching order matters with contains().

The current order is correct: /chat/completions is checked before /completions since both paths would match a /chat/completions request if the order were reversed. Consider using ends_with() or exact path matching for more precise routing, or add a comment explaining the ordering dependency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/guardrails/middleware.rs` around lines 30 - 37, The path-matching in
from_path uses contains() with an ordering dependency (e.g., "/chat/completions"
must be checked before "/completions"); update from_path to use more precise
checks (prefer using ends_with() or exact path matching for each route) or, if
you keep contains(), add a clarifying comment above from_path documenting the
ordering dependency and why "/chat/completions" must be matched first; reference
the from_path function and the "/chat/completions", "/completions", and
"/embeddings" match arms when making the change.

199-203: Inconsistent error handling: body buffer failure returns 400, but parse failures pass through.

When buffering fails (line 201), the middleware returns BAD_REQUEST. However, when JSON parsing fails (lines 212-214, 221-224, 231-234), the request is passed through to the inner service. This inconsistency could be confusing—consider either returning 400 for both cases or passing through for both.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/guardrails/middleware.rs` around lines 199 - 203, The middleware
currently returns BAD_REQUEST on buffer errors but lets JSON parse failures pass
through; make this consistent by changing the JSON parsing error branches (where
serde_json::from_slice is used) to return
axum::http::StatusCode::BAD_REQUEST.into_response() instead of forwarding to the
inner service, and log the parse error (similar to the existing debug message
"Guardrails middleware: failed to buffer request body, passing through") so
parse failures produce a 400 with error context instead of calling
inner.call(req).
src/pipelines/pipeline.rs (1)

140-149: Prefer match over consecutive if let for exhaustiveness.

The current pattern uses two separate if let blocks for NonStream and Stream variants. As noted in reviewer feedback, using match ensures exhaustiveness and future-proofs against new variants being added.

♻️ Proposed refactor to match
-            if let ChatCompletionResponse::NonStream(completion) = response {
-                tracer.log_success(&completion);
-                return Ok(Json(completion).into_response());
-            }
-
-            if let ChatCompletionResponse::Stream(stream) = response {
-                return Ok(Sse::new(trace_and_stream(tracer, stream))
-                    .keep_alive(KeepAlive::default())
-                    .into_response());
-            }
+            return match response {
+                ChatCompletionResponse::NonStream(completion) => {
+                    tracer.log_success(&completion);
+                    Ok(Json(completion).into_response())
+                }
+                ChatCompletionResponse::Stream(stream) => {
+                    Ok(Sse::new(trace_and_stream(tracer, stream))
+                        .keep_alive(KeepAlive::default())
+                        .into_response())
+                }
+            };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pipelines/pipeline.rs` around lines 140 - 149, Replace the two
consecutive if-let blocks matching on response with a single match on
ChatCompletionResponse to ensure exhaustiveness: match on response {
ChatCompletionResponse::NonStream(completion) => {
tracer.log_success(&completion); return Ok(Json(completion).into_response()); },
ChatCompletionResponse::Stream(stream) => { return
Ok(Sse::new(trace_and_stream(tracer,
stream)).keep_alive(KeepAlive::default()).into_response()); }, _ => { return
Err(/* return an appropriate error or conversion for unknown variants */) } } —
keep the existing calls to tracer.log_success, trace_and_stream, Json and
Sse/KeepAlive exactly as used now and add a fallback arm to handle any future
variants.
src/guardrails/parsing.rs (1)

24-31: Consider extracting shared content_to_string helper to reduce duplication.

The ChatMessageContent match logic is duplicated between PromptExtractor for ChatCompletionRequest (lines 24-31) and CompletionExtractor for ChatCompletion (lines 44-51). This aligns with reviewer feedback suggesting a shared helper.

♻️ Proposed helper extraction
+fn content_to_string(content: &ChatMessageContent) -> String {
+    match content {
+        ChatMessageContent::String(s) => s.clone(),
+        ChatMessageContent::Array(parts) => parts
+            .iter()
+            .filter(|p| p.r#type == "text")
+            .map(|p| p.text.as_str())
+            .collect::<Vec<_>>()
+            .join(" "),
+    }
+}
+
 impl PromptExtractor for ChatCompletionRequest {
     fn extract_prompt(&self) -> String {
         self.messages
             .iter()
             .filter_map(|m| {
-                m.content.as_ref().map(|content| match content {
-                    ChatMessageContent::String(s) => s.clone(),
-                    ChatMessageContent::Array(parts) => parts
-                        .iter()
-                        .filter(|p| p.r#type == "text")
-                        .map(|p| p.text.as_str())
-                        .collect::<Vec<_>>()
-                        .join(" "),
-                })
+                m.content.as_ref().map(content_to_string)
             })
             .collect::<Vec<_>>()
             .join("\n")
     }
 }

Also applies to: 44-51

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/guardrails/parsing.rs` around lines 24 - 31, Extract the duplicated
ChatMessageContent matching logic into a shared helper function (e.g.,
content_to_string) and use it from both PromptExtractor::... (where
ChatCompletionRequest messages are processed) and CompletionExtractor::...
(where ChatCompletion messages are processed); implement content_to_string to
accept &ChatMessageContent (or Option<&ChatMessageContent> if convenient) and
return a String by handling ChatMessageContent::String(s) -> s.clone() and
ChatMessageContent::Array(parts) -> join the text of parts with type == "text",
then replace the inline match blocks in both PromptExtractor and
CompletionExtractor with calls to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/guardrails/middleware.rs`:
- Line 84: The code is unboundedly buffering request and response bodies by
calling axum::body::to_bytes(..., usize::MAX) (seen in middleware.rs for
resp_body and at the request handling around line 197); replace usize::MAX with
a configurable max byte limit (e.g., a constant DEFAULT_MAX_BODY_BYTES = 10 *
1024 * 1024 or read from existing app config) and use that value in both places
(response handling where resp_body is converted and the request body conversion
at the other location) so large payloads are rejected instead of exhausting
memory; ensure you return a clear error (or early response) when to_bytes
returns an error or indicates the body exceeded the limit.

---

Outside diff comments:
In `@src/pipelines/pipeline.rs`:
- Around line 133-138: The chat_completions error path doesn't mark the OTEL
span as errored; update the inspect_err closure on the
model.chat_completions(payload.clone()).await call to call tracer.log_error(...)
(or the equivalent method on the tracer/span object in this module) with the
error details before the error is propagated, so the span is explicitly marked
as failed when the call returns Err; ensure you reference the same tracer/span
instance used for this pipeline span when calling tracer.log_error().

---

Nitpick comments:
In `@src/guardrails/middleware.rs`:
- Around line 30-37: The path-matching in from_path uses contains() with an
ordering dependency (e.g., "/chat/completions" must be checked before
"/completions"); update from_path to use more precise checks (prefer using
ends_with() or exact path matching for each route) or, if you keep contains(),
add a clarifying comment above from_path documenting the ordering dependency and
why "/chat/completions" must be matched first; reference the from_path function
and the "/chat/completions", "/completions", and "/embeddings" match arms when
making the change.
- Around line 199-203: The middleware currently returns BAD_REQUEST on buffer
errors but lets JSON parse failures pass through; make this consistent by
changing the JSON parsing error branches (where serde_json::from_slice is used)
to return axum::http::StatusCode::BAD_REQUEST.into_response() instead of
forwarding to the inner service, and log the parse error (similar to the
existing debug message "Guardrails middleware: failed to buffer request body,
passing through") so parse failures produce a 400 with error context instead of
calling inner.call(req).

In `@src/guardrails/parsing.rs`:
- Around line 24-31: Extract the duplicated ChatMessageContent matching logic
into a shared helper function (e.g., content_to_string) and use it from both
PromptExtractor::... (where ChatCompletionRequest messages are processed) and
CompletionExtractor::... (where ChatCompletion messages are processed);
implement content_to_string to accept &ChatMessageContent (or
Option<&ChatMessageContent> if convenient) and return a String by handling
ChatMessageContent::String(s) -> s.clone() and ChatMessageContent::Array(parts)
-> join the text of parts with type == "text", then replace the inline match
blocks in both PromptExtractor and CompletionExtractor with calls to this
helper.

In `@src/pipelines/pipeline.rs`:
- Around line 140-149: Replace the two consecutive if-let blocks matching on
response with a single match on ChatCompletionResponse to ensure exhaustiveness:
match on response { ChatCompletionResponse::NonStream(completion) => {
tracer.log_success(&completion); return Ok(Json(completion).into_response()); },
ChatCompletionResponse::Stream(stream) => { return
Ok(Sse::new(trace_and_stream(tracer,
stream)).keep_alive(KeepAlive::default()).into_response()); }, _ => { return
Err(/* return an appropriate error or conversion for unknown variants */) } } —
keep the existing calls to tracer.log_success, trace_and_stream, Json and
Sse/KeepAlive exactly as used now and add a fallback arm to handle any future
variants.

Copy link
Member

@doronkopit5 doronkopit5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Simplification Review (Items 1-13)

Inline comments below highlight 13 opportunities to make this code more idiomatic, readable, and performant. Ordered by impact — items 1-6 are highest priority, 7-13 are medium.


/// Shared guardrail resources: resolved guards + client.
/// Built once per router build and shared across all pipelines.
pub type GuardrailResources = (Arc<Vec<Guard>>, Arc<dyn GuardrailClient>);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #1 (HIGH): Replace tuple alias with a named struct

GuardrailResources is a type alias for (Arc<Vec<Guard>>, Arc<dyn GuardrailClient>). Accessing .0/.1 throughout setup.rs (lines 83-86, 96-98) is opaque and error-prone (field order can be silently swapped).

Suggested change:

pub struct GuardrailResources {
    pub guards: Arc<Vec<Guard>>,
    pub client: Arc<dyn GuardrailClient>,
}

Then shared.guards / shared.client instead of shared.0 / shared.1 everywhere.

TRACELOOP_PROVIDER => Some(Box::new(TraceloopClient::new())),
_ => None,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #2 (HIGH): Dead code — remove or integrate

create_guardrail_client is never called in production. build_guardrail_resources in setup.rs directly constructs TraceloopClient::new(), bypassing this function entirely.

Either:

  • Remove this function (and the test at test_traceloop_client.rs:157-161), or
  • Wire it into build_guardrail_resources so provider selection is actually dynamic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #3 (HIGH): Collapse 3 identical mock providers into 1

TestProviderOpenAI, TestProviderAnthropic, and TestProviderAzure are identical except for r#type() return value. This is ~150 lines of boilerplate.

Suggested refactor:

#[derive(Clone)]
struct TestProvider(ProviderType);

#[async_trait]
impl Provider for TestProvider {
    fn new(_config: &ProviderConfig) -> Self { Self(ProviderType::OpenAI) }
    fn key(&self) -> String { format!("{:?}-key", self.0).to_lowercase() }
    fn r#type(&self) -> ProviderType { self.0.clone() }
    // ... shared impls
}

The existing MockProviderForSpanTests already follows this parameterized pattern.


#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum GuardMode {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #5 (HIGH): Add Copy to fieldless enums

Both GuardMode and OnFailure (line 29) are fieldless enums — they are Copy-sized. Adding Copy to their derives eliminates unnecessary .clone() calls throughout runner.rs (e.g. line 151: on_failure: guard.on_failure.clone() becomes a simple copy).

self.evaluator_slug.hash(state);
// Hash params by sorting keys and hashing serialized values
let mut params_vec: Vec<_> = self.params.iter().collect();
params_vec.sort_by_key(|(k, _)| (*k).clone());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #6 (HIGH): Zero-copy hash sorting

sort_by_key(|(k, _)| (*k).clone()) allocates a new String per key on every hash call. Same issue on line 117 for GuardrailsConfig::hash.

Fix: Use sort_by(|a, b| a.0.cmp(&b.0)) — zero allocations, same ordering.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #10 (MEDIUM): Extract shared OTel span attribute helpers

The frequency_penalty/presence_penalty/top_p/temperature pattern (lines 231-248) is duplicated verbatim in CompletionRequest::record_span (lines 311-328). Also, the ChatMessageContent match block (lines 259-264) is duplicated in ChatCompletion::record_span (lines 288-294).

Suggested helpers:

fn set_optional_f64(span: &mut BoxedSpan, key: &'static str, value: Option<f32>) {
    if let Some(v) = value {
        span.set_attribute(KeyValue::new(key, v as f64));
    }
}

fn content_to_string(content: &ChatMessageContent) -> String {
    match content {
        ChatMessageContent::String(s) => s.clone(),
        ChatMessageContent::Array(parts) => serde_json::to_string(parts).unwrap_or_default(),
    }
}

src/state.rs Outdated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #11 (MEDIUM): Unnecessary full router clone on every request

Arc::try_unwrap(router).unwrap_or_else(|arc| (*arc).clone()) will almost always take the unwrap_or_else branch (the HashMap still holds a reference), cloning the entire Router on every single request.

Axum's Router is designed to be cheaply cloneable (inner state is already Arc-wrapped). Simply do:

let router: Router = (*router).clone();  // cheap Arc-bump clone

Or even better, call .oneshot() directly on the Arc<Router> via deref.

}
}

pub struct GuardPhaseResult {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #12 (MEDIUM): Consider Result instead of GuardPhaseResult

This struct is essentially Result<Vec<GuardWarning>, Response> — either you have a blocked response (error case) or you have warnings (success case). Using Result would let the middleware use the ? operator instead of if let Some(blocked) checks:

type GuardPhaseResult = Result<Vec<GuardWarning>, Response>;

Then outcome_to_phase_result becomes more natural and callers can use ? for early-return on block.

}

// Re-export builder and orchestrator functions for backward compatibility with tests
pub use crate::guardrails::runner::{blocked_response, warning_header_value};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMPLIFY #13 (MEDIUM): Remove unnecessary re-exports

These re-exports exist "for backward compatibility with tests" — but all the tests are new in this PR, so there is no backward compatibility concern. Import directly from the guardrails modules in test code and remove these re-exports to avoid a confusing indirection layer.

@doronkopit5
Copy link
Member

Code review

Found 13 issues:

  1. Streaming requests bypass ALL guardrails. Any caller can skip every guard -- including on_failure: block + required: true -- by setting "stream": true. Pre-call PII detection, prompt injection checks, all skipped. Streaming is the most common mode for LLM APIs.

// Skip guardrails for streaming requests
if parsed_request.is_streaming() {
debug!("Guardrails middleware: streaming request, skipping guardrails");
let request = Request::from_parts(parts, Body::from(bytes));
return inner.call(request).await;
}

  1. Unbounded memory allocation on body buffering. to_bytes(body, usize::MAX) disables the size limit that exists specifically to prevent OOM. A large upstream response will be buffered entirely into memory -- DoS vector on the gateway.

) -> Response {
let resp_bytes = match axum::body::to_bytes(resp_body, usize::MAX).await {
Ok(b) => b,

// Buffer request body
let bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(b) => b,

  1. with_remote_span_context misused for in-process parent-child spans. This OTel API is for spans received from external systems (W3C traceparent). Using it locally sets is_remote: true, causing tracing backends (Jaeger, Zipkin, Tempo) to show root and LLM spans as belonging to separate services. Should use Context::current_with_span(...) instead.

hub/src/pipelines/otel.rs

Lines 214 to 217 in b973e9e

/// suitable for creating child spans.
pub fn parent_context(&self) -> Context {
Context::current().with_remote_span_context(self.root_span.span_context().clone())
}

  1. build_guardrail_resources hardcodes TraceloopClient, bypassing the provider dispatch. The create_guardrail_client() function in providers/mod.rs correctly dispatches on guard.provider, but it is dead code -- never called from production. setup.rs always creates TraceloopClient::new() regardless of the configured provider. Any future non-Traceloop provider will silently be routed to Traceloop.

}
let all_guards = Arc::new(resolve_guard_defaults(config));
let client: Arc<dyn GuardrailClient> =
Arc::new(super::providers::traceloop::TraceloopClient::new());
Some((all_guards, client))

/// Create a guardrail client based on the guard's provider type.
pub fn create_guardrail_client(guard: &Guard) -> Option<Box<dyn GuardrailClient>> {
match guard.provider.as_str() {
TRACELOOP_PROVIDER => Some(Box::new(TraceloopClient::new())),
_ => None,
}
}

  1. unsafe { std::env::set_var } in async tests without serialization. Two tests mutate global env vars in unsafe blocks -- one in a #[tokio::test] on a thread pool. In Rust 1.80+, set_var is unsafe because it is not thread-safe. This is UB when tests run in parallel. Same issue was flagged in PR fix(config): allow env vars #58.

unsafe {
std::env::set_var("E2E_TEST_API_KEY", "resolved-key-123");
}

fn test_guard_config_env_var_in_api_key() {
unsafe {
std::env::set_var("TEST_GUARD_API_KEY_UNIQUE", "tl-secret-key");
}

  1. GUARDRAILS.md contract violated: no warning header on non-required guard errors. Documentation states evaluator errors with required: false should "Add warning header, continue (fail-open)". But the Err branch for non-required guards pushes a GuardResult::Error without ever pushing a GuardWarning. No X-Traceloop-Guardrail-Warning header is emitted -- errors on optional guards are silently swallowed.

Err(err) => {
let is_required = guard.required;
results.push(GuardResult::Error {
name: guard.name.clone(),
error: err.to_string(),
required: is_required,
});
if is_required {
blocked = true;
if blocking_guard.is_none() {
blocking_guard = Some(guard.name.clone());
}
}

  1. X-Traceloop-Guardrails header resolves guards from all pipelines globally. Guard names from the header are resolved against gr.all_guards (every guard across all pipelines), not scoped to the current pipeline. A caller can activate guards intended for a different pipeline.

/// Resolve guards for this request by merging pipeline guards with header-specified guards.
fn resolve_request_guards(gr: &Guardrails, headers: &HeaderMap) -> (Vec<Guard>, Vec<Guard>) {
let header_guard_names = headers
.get("x-traceloop-guardrails")
.and_then(|v| v.to_str().ok())
.map(parse_guardrails_header)
.unwrap_or_default();
let pipeline_names: Vec<&str> = gr.pipeline_guard_names.iter().map(|s| s.as_str()).collect();
let header_names: Vec<&str> = header_guard_names.iter().map(|s| s.as_str()).collect();
let resolved = resolve_guards_by_name(&gr.all_guards, &pipeline_names, &header_names);

  1. Cascading validation errors: 3 errors when 1 would suffice. When a guard references a non-existent provider, users get "non-existent provider", "no api_base", and "no api_key" -- the latter two are consequences of the first. Consider short-circuiting with continue after the provider check fails.

// Check provider reference exists
if !gr_config.providers.contains_key(&guard.provider) {
errors.push(format!(
"Guard '{}' references non-existent guardrail provider '{}'.",
guard.name, guard.provider
));
}
// Check api_base and api_key (either directly or via provider)
let has_api_base = guard.api_base.as_ref().is_some_and(|s| !s.is_empty())
|| gr_config
.providers
.get(&guard.provider)
.is_some_and(|p| !p.api_base.is_empty());
let has_api_key = guard.api_key.as_ref().is_some_and(|s| !s.is_empty())
|| gr_config
.providers
.get(&guard.provider)
.is_some_and(|p| !p.api_key.is_empty());
if !has_api_base {
errors.push(format!(
"Guard '{}' has no api_base configured (neither on the guard nor on provider '{}').",
guard.name, guard.provider
));
}
if !has_api_key {
errors.push(format!(
"Guard '{}' has no api_key configured (neither on the guard nor on provider '{}').",
guard.name, guard.provider

  1. OTel request attributes lost on error path (regression). Before this PR, OtelTracer::start() immediately recorded request attributes (model, prompt, temperature). Now start() creates a bare span; attributes only appear in start_llm_span() which is only called on the success path. Error spans have zero request attributes.

hub/src/pipelines/otel.rs

Lines 94 to 100 in b973e9e

pub fn start() -> Self {
let tracer = global::tracer("traceloop_hub");
let span = tracer
.span_builder("traceloop_hub")
.with_kind(SpanKind::Server)
.start(&tracer);

hub/src/pipelines/otel.rs

Lines 120 to 123 in b973e9e

pub fn start_llm_span<T: RecordSpan>(&mut self, operation: &str, request: &T) {
let tracer = global::tracer("traceloop_hub");
let parent_cx = self.parent_context();
let mut span = tracer

  1. Misleading "passing through" comment returns 400. Debug log says "passing through" but the code returns BAD_REQUEST with no body. Either pass through as described, or fix the log and return a JSON error body.

Err(_) => {
debug!("Guardrails middleware: failed to buffer request body, passing through");
return Ok(axum::http::StatusCode::BAD_REQUEST.into_response());
}

  1. Wrong test cassette for secrets detector. secrets_detector_pass.json uses the text "You are a complete idiot and everyone hates you" -- which is toxicity text, not secrets-related. Same string as toxicity_detector_fail.json. The test passes only due to VCR pre-recording and validates nothing meaningful about secrets detection.

"input_text": "You are a complete idiot and everyone hates you. You should be ashamed.",

  1. Doc comment describes wrong URL path. Comment says POST {api_base}/v2/guardrails/{evaluator_slug} but code constructs /v2/guardrails/execute/{evaluator_slug} -- missing /execute/.

/// HTTP client for the Traceloop evaluator API service.
/// Calls `POST {api_base}/v2/guardrails/{evaluator_slug}`.
pub struct TraceloopClient {

let url = format!(
"{}/v2/guardrails/execute/{}",
api_base, guard.evaluator_slug

  1. Redundant Content-Type header alongside .json(). reqwest's .json() already sets Content-Type: application/json. The explicit .header() call is redundant.

.header("Authorization", format!("Bearer {api_key}"))
.header("Content-Type", "application/json")
.json(&body)

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

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.

4 participants