Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions docs/enterprise/guardrails/redaction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,6 @@ For streaming requests that declare tools, runtime redaction holds output until

MCP-targeted rules continue to evaluate actual MCP execution arguments and results through their existing adapters. Provider-managed transformations and Lakera retain their existing argument-mapping restrictions.

## LLM Tool-Call Arguments

Custom Regex, Secrets Detection, Microsoft Presidio, and Azure AI Language PII include tool-call arguments by default when an LLM rule applies. Input rules cover assistant tool calls in the selected conversation history; output rules cover calls generated by the model. This includes Chat function arguments and Responses function arguments or custom-tool input. Tool names, IDs, and definitions are not redaction targets.

Arguments use the same redaction strategy and mode as other fields. Runtime redaction can change what a bash, grep, or other command does; Bifrost returns the redacted arguments without repairing or restoring the command. `logs_only` preserves the call sent to the client and records redaction mappings for logs and traces.

For streaming requests that declare tools, runtime redaction holds output until the complete arguments have been evaluated. Bifrost rewrites argument deltas and terminal copies before replay; it does not call the guardrail provider for every argument fragment. This adds holdback latency. Requests without tools retain the existing text-segment behavior. Under active runtime redaction, unexpected tool calls on requests without declared tools or tool-call history are rejected, including calls arriving before any text.

MCP-targeted rules continue to evaluate actual MCP execution arguments and results through their existing adapters. Provider-managed transformations and Lakera retain their existing argument-mapping restrictions.

## Edge Cases

- Redaction is text-based. It does not inspect image pixels, audio, or arbitrary binary content.
Expand Down
13 changes: 7 additions & 6 deletions plugins/routing/complexity/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ const (
// InputBypass means the operation is unsupported or explicitly belongs to a
// harness background workload. It neither classifies nor refreshes a session.
InputBypass InputDisposition = iota
// InputContinuation means a supported conversational request contains no new
// classifiable human text. It may reuse existing session state but cannot
// create or escalate it.
// InputContinuation means a supported conversational request is continuing an
// earlier turn. It may reuse existing session state; when that state is not
// available, LastUserText retains the recoverable task for a safe fallback
// classification.
InputContinuation
// InputClassifiable means the request contains human-authored text that may
// initialize or escalate a session tier.
Expand Down Expand Up @@ -126,7 +127,7 @@ func BuildInputWithDisposition(ctx *schemas.BifrostContext, req *schemas.Bifrost
return ComplexityInput{}, InputContinuation
}
if chatHasTrailingContinuation(req.ChatRequest.Input, harness) {
return ComplexityInput{}, InputContinuation
return input, InputContinuation
}
return input, InputClassifiable
case schemas.TextCompletionRequest, schemas.TextCompletionStreamRequest:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand All @@ -147,7 +148,7 @@ func BuildInputWithDisposition(ctx *schemas.BifrostContext, req *schemas.Bifrost
return ComplexityInput{}, InputContinuation
}
if responsesHasTrailingContinuation(req.ResponsesRequest.Input, harness) {
return ComplexityInput{}, InputContinuation
return input, InputContinuation
}
return input, InputClassifiable
default:
Expand Down Expand Up @@ -234,7 +235,7 @@ func extractFromChatMessages(messages []schemas.ChatMessage, harness complexityH
case schemas.ChatMessageRoleUser:
text, ok := extractChatTextOnly(msg.Content)
if !ok {
return ComplexityInput{}, false
continue
}
text, kind := sanitizeUserText(text, harness)
switch kind {
Expand Down
34 changes: 27 additions & 7 deletions plugins/routing/complexity/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,10 +686,11 @@ func TestBuildInputWithDisposition(t *testing.T) {
userRole := schemas.ResponsesInputMessageRoleUser
assistantRole := schemas.ResponsesInputMessageRoleAssistant
tests := []struct {
name string
ctx *schemas.BifrostContext
req *schemas.BifrostRequest
want InputDisposition
name string
ctx *schemas.BifrostContext
req *schemas.BifrostRequest
want InputDisposition
wantText string
}{
{
name: "human turn is classifiable",
Expand Down Expand Up @@ -717,6 +718,20 @@ func TestBuildInputWithDisposition(t *testing.T) {
},
want: InputClassifiable,
},
{
name: "chat textless user fragment does not hide earlier human turn",
req: &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{Input: []schemas.ChatMessage{
{Role: schemas.ChatMessageRoleUser, Content: complexityChatString("Inspect the attached image")},
{Role: schemas.ChatMessageRoleUser, Content: complexityChatBlocks(
schemas.ChatContentBlock{Type: schemas.ChatContentBlockTypeImage},
)},
}},
},
want: InputContinuation,
wantText: "Inspect the attached image",
},
{
name: "claude code text and image in one turn is classifiable",
ctx: complexityHarnessContext(schemas.ClaudeCLI.String(), nil),
Expand Down Expand Up @@ -751,7 +766,8 @@ func TestBuildInputWithDisposition(t *testing.T) {
{Role: schemas.ChatMessageRoleTool, Content: complexityChatString("Tests passed")},
}},
},
want: InputContinuation,
want: InputContinuation,
wantText: "Run the tests",
},
{
name: "responses replay followed by tool output is a continuation",
Expand All @@ -765,7 +781,8 @@ func TestBuildInputWithDisposition(t *testing.T) {
}},
}
}(),
want: InputContinuation,
want: InputContinuation,
wantText: "Run the tests",
},
{
name: "unsupported operation bypasses session state",
Expand All @@ -789,8 +806,11 @@ func TestBuildInputWithDisposition(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, got := BuildInputWithDisposition(tt.ctx, tt.req)
input, got := BuildInputWithDisposition(tt.ctx, tt.req)
assert.Equal(t, tt.want, got)
if tt.wantText != "" {
assert.Equal(t, tt.wantText, input.LastUserText)
}
})
}
}
Expand Down
6 changes: 3 additions & 3 deletions plugins/routing/complexity/prerequesthook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ func TestPreRequestHook_SessionStoreFailureFallsBackToCurrentClassification(t *t
require.Equal(t, complexity.MechanismSemantic, ctx.Value(schemas.BifrostContextKeyGovernanceComplexityMechanism))
}

func TestPreRequestHook_SessionContinuationReusesButDoesNotInitializeTier(t *testing.T) {
func TestPreRequestHook_SessionContinuationReusesOrClassifiesRecoveredTask(t *testing.T) {
plugin := newSessionComplexityRuleFixture(t)
plugin.SetEmbeddingRequestExecutor(testEmbeddingExecutor)
require.NoError(t, plugin.ReloadComplexityAnalyzerConfig(sessionAnalyzerConfig()))
Expand All @@ -360,8 +360,8 @@ func TestPreRequestHook_SessionContinuationReusesButDoesNotInitializeTier(t *tes

absentCtx := complexitySessionContext("new-session")
require.NoError(t, plugin.PreRequestHook(absentCtx, continuationRequest()))
require.Nil(t, absentCtx.Value(schemas.BifrostContextKeyGovernanceComplexityTier))
require.Equal(t, complexity.MechanismSkipped, absentCtx.Value(schemas.BifrostContextKeyGovernanceComplexityMechanism))
require.Equal(t, complexity.TierComplex, absentCtx.Value(schemas.BifrostContextKeyGovernanceComplexityTier))
require.Equal(t, complexity.MechanismSemantic, absentCtx.Value(schemas.BifrostContextKeyGovernanceComplexityMechanism))

initialCtx := complexitySessionContext("existing-session")
require.NoError(t, plugin.PreRequestHook(initialCtx, chatRequest("a medium request")))
Expand Down
14 changes: 12 additions & 2 deletions plugins/routing/complexityrouting.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func (p *RoutingPlugin) computeComplexity(
sessionID, _ := ctx.Value(schemas.BifrostContextKeySessionID).(string)
sessionActive := p.sessionEnabled.Load() && sessionID != "" && p.sessionStore != nil

if disposition != complexity.InputClassifiable {
if sessionActive && disposition == complexity.InputContinuation {
if disposition == complexity.InputContinuation {
if sessionActive {
key := buildComplexitySessionKey(ctx, virtualKeyID, sessionID)
tier, found, err := p.sessionStore.load(key, true)
if err != nil {
Expand All @@ -48,6 +48,16 @@ func (p *RoutingPlugin) computeComplexity(
return result
}
}
if input.LastUserText == "" {
publishComplexityDecision(ctx, nil, complexity.MechanismSkipped, nil)
ctx.AppendRoutingEngineLog(
schemas.RoutingEngineRoutingRule,
schemas.LogLevelInfo,
noClassifiableComplexityInputLog,
)
return nil
}
} else if disposition != complexity.InputClassifiable {

publishComplexityDecision(ctx, nil, complexity.MechanismSkipped, nil)
ctx.AppendRoutingEngineLog(
Expand Down
Loading