Skip to content

fix(guardrails): enforce admission around prompt guardrails and refuse unguarded passthrough - #972

Open
SantiagoDePolonia wants to merge 5 commits into
mainfrom
fix/guardrail-enforcement-gaps
Open

fix(guardrails): enforce admission around prompt guardrails and refuse unguarded passthrough#972
SantiagoDePolonia wants to merge 5 commits into
mainfrom
fix/guardrail-enforcement-gaps

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Two guardrail enforcement gaps found in pre-release testing.

Prompt-phase guardrail decisions bypassed rate limits and budgets

A prompt chain that blocks or answers a request returned before handleWithCache, and admission only ran inside dispatch — so a blocked or guardrail-answered request consumed no rate-limit token and passed no budget check, while an llm_judge step had already made a real, billed model call to decide it. Verified live: under a 3 req/min limit that 429s benign traffic, guardrail-answered requests returned 200 indefinitely, over budget too.

Ordering chosen: when a request runs a prompt-phase chain, admission (rate limits, then budget) now runs as soon as the route is resolved and before the chain. Money is never spent on a judge call for a request the gateway will not serve, and a decision the chain makes is still counted. Admission is granted once per request and reused by dispatch, so nothing is counted twice.

The gate is conditional on the request actually running a prompt chain. Without one nothing is spent or decided before dispatch, so admission stays where it was and response-cache hits remain free, as docs/features/rate-limits.mdx and docs/features/budgets.mdx document. With a prompt chain the cache is consulted only after the chain has already been paid for, so counting those hits is the consistent choice; both docs now say so.

/p/{provider}/... passthrough ran no guardrails, and nothing said so

The same key that got 400 keyword_block on /v1/chat/completions got 200 on /p/openai/v1/chat/completions and /p/deepseek/v1/chat/completions. Passthrough routes are on by default.

Running the chains there faithfully is not possible: passthrough forwards arbitrary provider-native bodies (OpenAI, Anthropic, Gemini, Cohere dialects, and non-chat endpoints) and relays the provider's answer byte-for-byte, which is the point of the route. So the gap is made explicit and fails safe instead: a passthrough request is refused with 403 passthrough_guardrails_unsupported whenever a guardrail workflow applies to the caller. Operators who accept the gap set server.allow_unguarded_passthrough: true (ALLOW_UNGUARDED_PASSTHROUGH=true), or scope the workflow so it does not match those callers. Passthrough is unchanged for every caller no guardrail workflow matches, and for every non-inference passthrough route (model lists, files), which run no guardrail on /v1 either.

The workflow of a passthrough request is matched on the model read from its provider-native body, and that read is best effort. So a text-inference passthrough call whose model GoModel cannot read (chunked, oversized, or an unparsed dialect) is refused whenever any active workflow runs a guardrail chain — otherwise a caller could escape a model-scoped policy just by making the body unreadable.

Which routes count as text inference is decided by the route's GenAI operation, and by the endpoint path when the provider ships no passthrough semantics table (hetzner is enabled by default and has none) or the table does not name the endpoint. Otherwise the refusal would have been escapable by sending the same body to such a provider, or to a legacy /completions route. Model lists, token counts, embeddings, files and batches keep passing through.

User-visible impact

  • Guardrail blocks and guardrail-answered requests now count against rate limits and budgets; an over-limit or over-budget request is refused before any judge call.
  • A response-cache hit counts only when the request runs a prompt-phase guardrail; otherwise it is still free.
  • New workflows.Service.HasGuardrailChains reports whether any active workflow runs guardrails; passthrough uses it for the unreadable-model case.
  • New server.allow_unguarded_passthrough / ALLOW_UNGUARDED_PASSTHROUGH, default false. Deployments that use both guardrails and passthrough for the same callers get 403 until they opt in or rescope the workflow.
  • Docs updated: docs/advanced/guardrails.mdx, docs/features/passthrough-api.mdx, docs/features/rate-limits.mdx, docs/features/budgets.mdx, .env.template, config/config.example.yaml.

Testing

  • New internal/server/guardrail_admission_test.go: block/respond decisions consume the limit, an over-budget request never reaches the prompt hook, an allowed request is counted once, admitOnce grants a single admission, the passthrough refusal / opt-out / no-chains / unreadable-model / non-inference cases, and internal/workflows: HasGuardrailChains with and without a guarded workflow. All three admission subtests fail on main.
  • go build ./..., go test -race ./internal/server ./internal/gateway ./config ./internal/app ./internal/guardrails, go test ./..., make lint, hot-path perf guard.
  • Live against a gateway with a prompt-phase blocking guardrail, a 2 req/min user-path limit and a real provider: blocked prompts 400 400 429 429 429 (was 400 ×5); benign traffic under a 3/min limit 200 200 200 429 with requests_used: 3; /p/deepseek/v1/chat/completions and /p/deepseek/chat/completions 403 passthrough_guardrails_unsupported, 200 again with ALLOW_UNGUARDED_PASSTHROUGH=true; the same chat body sent with Transfer-Encoding: chunked also 403, while /p/deepseek/v1/models stays 200.

@mintlify

mintlify Bot commented Sep 12, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
gomodel 🟢 Ready View Preview Sep 12, 2026, 8:44 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 45 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2251cff4-b39a-4898-9e0a-6b441d916e2f

📥 Commits

Reviewing files that changed from the base of the PR and between ac672ad and 858f486.

📒 Files selected for processing (21)
  • .env.template
  • config/config.example.yaml
  • config/server.go
  • docs/advanced/guardrails.mdx
  • docs/features/budgets.mdx
  • docs/features/passthrough-api.mdx
  • docs/features/rate-limits.mdx
  • internal/app/init_server.go
  • internal/gateway/inference_orchestrator.go
  • internal/gateway/inference_prepare.go
  • internal/server/guardrail_admission_test.go
  • internal/server/handlers.go
  • internal/server/handlers_test.go
  • internal/server/http.go
  • internal/server/messages_handler.go
  • internal/server/messages_native.go
  • internal/server/passthrough_service.go
  • internal/server/ratelimit_support.go
  • internal/server/translated_inference_service.go
  • internal/workflows/guardrail_presence_test.go
  • internal/workflows/service.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov-commenter

codecov-commenter commented Sep 12, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 92.00000% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/gateway/inference_prepare.go 0.00% 5 Missing ⚠️
internal/server/passthrough_service.go 96.55% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

Safe to merge.

Reviews (2) · Last reviewed commit: "fix(passthrough): refuse unguarded infer..."

Comment thread internal/server/passthrough_service.go Outdated
…te semantics

Only a provider shipping a passthrough semantics table names a GenAI operation, so the guardrail refusal missed text inference on every other provider (hetzner is passthrough-enabled by default and has no table) and on the legacy completion endpoints. The endpoint path now decides when the operation is unknown.
@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Rate Limit Exceeded

@SantiagoDePolonia have exceeded the limit for the number of chat messages per hour. Please wait 12 minutes and 42 seconds before sending another message.

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.

2 participants