Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b8a4783
[Feature] Move response jailbreak detection into a response-stage signal
subin9 Sep 2, 2026
9bcdc00
[Feature] Put the response stage on the jailbreak rule and evaluate r…
subin9 Sep 2, 2026
3d8142f
[Feature] Send the compose probe the keyword signal matches and split…
subin9 Sep 3, 2026
e458187
[Feature] Keep the dashboard jailbreak field out of the oversized sig…
subin9 Sep 4, 2026
e7aa683
[Feature] Let the selected decision's plugin consume the response signal
subin9 Sep 4, 2026
f642841
[Feature] Record the response-stage observation in Router Replay and …
subin9 Sep 5, 2026
1042aa7
Merge remote-tracking branch 'origin/main' into feat/response-stage-o…
subin9 Sep 5, 2026
d8d158e
[CI/Build] Tidy dashboard/backend go.sum for the router module's Rout…
subin9 Sep 5, 2026
bcb9686
[Bug] Provision the jailbreak classifier for its response-stage consu…
subin9 Sep 5, 2026
234dbae
Merge remote-tracking branch 'origin/main' into feat/response-stage-o…
subin9 Sep 5, 2026
202bff2
[Bug] Leave a partly scanned response unresolved instead of clean
subin9 Sep 5, 2026
09b5f62
[Bug] Reject a response-direction rule read through a projection
subin9 Sep 5, 2026
8d47ea7
Merge branch 'main' into feat/response-stage-observation
subin9 Sep 6, 2026
53e8f2c
Merge branch 'main' into feat/response-stage-observation
subin9 Sep 6, 2026
1a4ff8e
[Bug] Resolve a partly scanned response per jailbreak rule threshold
subin9 Sep 7, 2026
52412a4
Merge remote-tracking branch 'origin/main' into feat/response-stage-o…
subin9 Sep 7, 2026
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
7 changes: 7 additions & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,13 @@ routing:
benign_patterns:
- explain the policy
- summarize the safety rules
# direction: response scores the model's output instead of the prompt, so
# this rule only exists once the model has answered; the selected
# decision's response_jailbreak plugin enforces on it.
- name: unsafe_completion
direction: response
threshold: 0.85
description: Detect jailbreak content in the model's own output.

pii:
- name: restricted_pii
Expand Down
12 changes: 12 additions & 0 deletions config/fragments/signal/jailbreak/response.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Response-direction jailbreak signal.
#
# Scores the model's output rather than the request, so it is only available
# after the model has answered. The response_jailbreak plugin of the decision
# selected for the request enforces on it; decision rules cannot read it.
routing:
signals:
jailbreak:
- name: unsafe_completion
direction: response
threshold: 0.85
description: Detect jailbreak content in the model's own output.
85 changes: 47 additions & 38 deletions dashboard/frontend/src/lib/dslSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,52 @@ import { getCapabilityPluginFieldSchema } from './dslCapabilityPluginSchemas'
import type { FieldSchema } from './dslSchemaTypes'
export type { FieldSchema } from './dslSchemaTypes'

const JAILBREAK_SIGNAL_FIELDS: FieldSchema[] = [
{
key: 'method',
label: 'Method',
type: 'select',
options: ['classifier', 'contrastive'],
description: 'Detection algorithm',
},
{
key: 'direction',
label: 'Direction',
type: 'select',
options: ['request', 'response'],
description: 'request (default) scores the prompt; response scores the model output',
},
{
key: 'threshold',
label: 'Threshold',
type: 'number',
required: true,
placeholder: '0.9',
description: 'Minimum score to trigger (0.0-1.0)',
},
{
key: 'include_history',
label: 'Include History',
type: 'boolean',
description: 'Include conversation history in detection',
},
{ key: 'description', label: 'Description', type: 'string' },
{
key: 'jailbreak_patterns',
label: 'Jailbreak Patterns',
type: 'string[]',
placeholder: 'Add jailbreak example...',
description: 'Contrastive mode: example jailbreak prompts',
},
{
key: 'benign_patterns',
label: 'Benign Patterns',
type: 'string[]',
placeholder: 'Add benign example...',
description: 'Contrastive mode: example benign prompts',
},
]

export function getSignalFieldSchema(signalType: string): FieldSchema[] {
const policyFields = getPolicySignalFieldSchema(signalType)
if (policyFields) return policyFields
Expand Down Expand Up @@ -263,44 +309,7 @@ export function getSignalFieldSchema(signalType: string): FieldSchema[] {
{ key: 'description', label: 'Description', type: 'string' },
]
case 'jailbreak':
return [
{
key: 'method',
label: 'Method',
type: 'select',
options: ['classifier', 'contrastive'],
description: 'Detection algorithm',
},
{
key: 'threshold',
label: 'Threshold',
type: 'number',
required: true,
placeholder: '0.9',
description: 'Minimum score to trigger (0.0-1.0)',
},
{
key: 'include_history',
label: 'Include History',
type: 'boolean',
description: 'Include conversation history in detection',
},
{ key: 'description', label: 'Description', type: 'string' },
{
key: 'jailbreak_patterns',
label: 'Jailbreak Patterns',
type: 'string[]',
placeholder: 'Add jailbreak example...',
description: 'Contrastive mode: example jailbreak prompts',
},
{
key: 'benign_patterns',
label: 'Benign Patterns',
type: 'string[]',
placeholder: 'Add benign example...',
description: 'Contrastive mode: example benign prompts',
},
]
return JAILBREAK_SIGNAL_FIELDS
case 'pii':
return [
{
Expand Down
214 changes: 128 additions & 86 deletions dashboard/frontend/src/pages/ConfigPageSignalsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,126 @@ interface ConfigPageSignalsSectionProps {
removeSignalByName: (cfg: ConfigData, type: SignalType, targetName: string) => void
}

function jailbreakDetailFields(
rawData: UnifiedSignal['rawData'],
): Array<{ label: string; value: React.ReactNode; fullWidth?: boolean }> {
const fields: Array<{ label: string; value: React.ReactNode; fullWidth?: boolean }> = [
{ label: 'Method', value: rawData.method || 'classifier', fullWidth: true },
{
label: 'Threshold',
value: rawData.threshold?.toString() || 'N/A',
fullWidth: true,
},
{
label: 'Direction',
value: rawData.direction || 'request',
fullWidth: true,
},
{
label: 'Include History',
value: rawData.include_history ? 'Yes' : 'No',
fullWidth: true,
},
]
if (rawData.method === 'contrastive') {
fields.push(
{
label: 'Jailbreak Patterns',
value: (
<SignalStringListEditor
value={rawData.jailbreak_patterns}
onChange={() => undefined}
addLabel=""
emptyLabel="No jailbreak patterns."
itemLabel="Jailbreak pattern"
readOnly
/>
),
fullWidth: true,
},
{
label: 'Benign Patterns',
value: (
<SignalStringListEditor
value={rawData.benign_patterns}
onChange={() => undefined}
addLabel=""
emptyLabel="No benign patterns."
itemLabel="Benign pattern"
readOnly
/>
),
fullWidth: true,
},
)
}
fields.push({
label: 'Description',
value: rawData.description || 'N/A',
fullWidth: true,
})
return fields
}

type JailbreakFormState = Pick<
AddSignalFormState,
| 'jailbreak_threshold'
| 'jailbreak_method'
| 'jailbreak_direction'
| 'include_history'
| 'jailbreak_patterns'
| 'benign_patterns'
>

function jailbreakFormDefaults(): JailbreakFormState {
return {
jailbreak_threshold: 0.65,
jailbreak_method: 'classifier',
jailbreak_direction: 'request',
include_history: false,
jailbreak_patterns: [],
benign_patterns: [],
}
}

function jailbreakFormStateFrom(rawData: UnifiedSignal['rawData']): JailbreakFormState {
return {
jailbreak_threshold: rawData.threshold ?? 0.65,
jailbreak_method: rawData.method || 'classifier',
jailbreak_direction: rawData.direction || 'request',
include_history: !!rawData.include_history,
jailbreak_patterns: [...(rawData.jailbreak_patterns || [])],
benign_patterns: [...(rawData.benign_patterns || [])],
}
}

function buildJailbreakEntry(name: string, formData: AddSignalFormState): JailbreakSignal {
const jailbreak_threshold = formData.jailbreak_threshold ?? 0.65
if (jailbreak_threshold < 0 || jailbreak_threshold > 1) {
throw new Error('Jailbreak threshold must be between 0.0 and 1.0.')
}
const method = formData.jailbreak_method || 'classifier'
const jailbreakEntry: JailbreakSignal = {
name,
threshold: jailbreak_threshold,
include_history: formData.include_history || false,
description: formData.description || undefined,
}
if (method !== 'classifier') {
jailbreakEntry.method = method
}
if (formData.jailbreak_direction === 'response') {
jailbreakEntry.direction = 'response'
}
if (method === 'contrastive') {
const jailbreakPatterns = normalizeStringList(formData.jailbreak_patterns, 'Jailbreak patterns')
const benignPatterns = normalizeStringList(formData.benign_patterns, 'Benign patterns')
if (jailbreakPatterns.length > 0) jailbreakEntry.jailbreak_patterns = jailbreakPatterns
if (benignPatterns.length > 0) jailbreakEntry.benign_patterns = benignPatterns
}
return jailbreakEntry
}

export default function ConfigPageSignalsSection({
config,
isPythonCLI,
Expand Down Expand Up @@ -265,7 +385,7 @@ export default function ConfigPageSignalsSection({
allSignals.push({
name: jb.name,
type: 'Jailbreak',
summary: `Method: ${method}, Threshold: ${jb.threshold}${jb.include_history ? ', includes history' : ''}`,
summary: `Method: ${method}, Threshold: ${jb.threshold}${jb.direction === 'response' ? ', scores the response' : ''}${jb.include_history ? ', includes history' : ''}`,
rawData: jb,
})
})
Expand Down Expand Up @@ -698,57 +818,7 @@ export default function ConfigPageSignalsSection({
],
})
} else if (signal.type === 'Jailbreak') {
const fields: Array<{ label: string; value: React.ReactNode; fullWidth?: boolean }> = [
{ label: 'Method', value: signal.rawData.method || 'classifier', fullWidth: true },
{
label: 'Threshold',
value: signal.rawData.threshold?.toString() || 'N/A',
fullWidth: true,
},
{
label: 'Include History',
value: signal.rawData.include_history ? 'Yes' : 'No',
fullWidth: true,
},
]
if (signal.rawData.method === 'contrastive') {
fields.push(
{
label: 'Jailbreak Patterns',
value: (
<SignalStringListEditor
value={signal.rawData.jailbreak_patterns}
onChange={() => undefined}
addLabel=""
emptyLabel="No jailbreak patterns."
itemLabel="Jailbreak pattern"
readOnly
/>
),
fullWidth: true,
},
{
label: 'Benign Patterns',
value: (
<SignalStringListEditor
value={signal.rawData.benign_patterns}
onChange={() => undefined}
addLabel=""
emptyLabel="No benign patterns."
itemLabel="Benign pattern"
readOnly
/>
),
fullWidth: true,
},
)
}
fields.push({
label: 'Description',
value: signal.rawData.description || 'N/A',
fullWidth: true,
})
sections.push({ title: 'Jailbreak Signal', fields })
sections.push({ title: 'Jailbreak Signal', fields: jailbreakDetailFields(signal.rawData) })
} else if (signal.type === 'PII') {
sections.push({
title: 'PII Signal',
Expand Down Expand Up @@ -857,11 +927,7 @@ export default function ConfigPageSignalsSection({
easy_candidates: [],
composer_operator: 'AND',
composer_conditions: [],
jailbreak_threshold: 0.65,
jailbreak_method: 'classifier',
include_history: false,
jailbreak_patterns: [],
benign_patterns: [],
...jailbreakFormDefaults(),
pii_threshold: 0.5,
pii_types_allowed: [],
pii_include_history: false,
Expand Down Expand Up @@ -921,11 +987,7 @@ export default function ConfigPageSignalsSection({
easy_candidates: [...(signal.rawData.easy?.candidates || [])],
composer_operator: signal.rawData.composer?.operator || 'AND',
composer_conditions: [...(signal.rawData.composer?.conditions || [])],
jailbreak_threshold: signal.rawData.threshold ?? 0.65,
jailbreak_method: signal.rawData.method || 'classifier',
include_history: !!signal.rawData.include_history,
jailbreak_patterns: [...(signal.rawData.jailbreak_patterns || [])],
benign_patterns: [...(signal.rawData.benign_patterns || [])],
...jailbreakFormStateFrom(signal.rawData),
pii_threshold: signal.rawData.threshold ?? 0.5,
pii_types_allowed: [...(signal.rawData.pii_types_allowed || [])],
pii_include_history: !!signal.rawData.include_history,
Expand Down Expand Up @@ -1213,30 +1275,10 @@ export default function ConfigPageSignalsSection({
break
}
case 'Jailbreak': {
const jailbreak_threshold = formData.jailbreak_threshold ?? 0.65
if (jailbreak_threshold < 0 || jailbreak_threshold > 1) {
throw new Error('Jailbreak threshold must be between 0.0 and 1.0.')
}
const method = formData.jailbreak_method || 'classifier'
const jailbreakEntry: JailbreakSignal = {
name,
threshold: jailbreak_threshold,
include_history: formData.include_history || false,
description: formData.description || undefined,
}
if (method !== 'classifier') {
jailbreakEntry.method = method
}
if (method === 'contrastive') {
const jailbreakPatterns = normalizeStringList(
formData.jailbreak_patterns,
'Jailbreak patterns',
)
const benignPatterns = normalizeStringList(formData.benign_patterns, 'Benign patterns')
if (jailbreakPatterns.length > 0) jailbreakEntry.jailbreak_patterns = jailbreakPatterns
if (benignPatterns.length > 0) jailbreakEntry.benign_patterns = benignPatterns
}
newConfig.signals.jailbreak = [...(newConfig.signals.jailbreak || []), jailbreakEntry]
newConfig.signals.jailbreak = [
...(newConfig.signals.jailbreak || []),
buildJailbreakEntry(name, formData),
]
break
}
case 'PII': {
Expand Down
Loading
Loading