feat(harness): 输入护栏升级 —— 归一化反绕过(Tier1) + LLM 越狱分类器(Tier2)

原输入护栏纯正则,空格/编码/同形字一改写即漏,且 bannedTerms 空置。升级为两层:

Tier1(网关同步、无 LLM):先归一化再匹配,干掉绕过——
- 小写 + 去零宽字符 + 去变音符 + 同形字折叠(西里尔/希腊→拉丁) +
  拆字间隔还原(i g n o r e / i.g.n.o.r.e → ignore) + base64 解码回扫
- 多视图(原文/归一化/紧凑/解码)匹配高精度注入正则,无需穷举变体
- bannedTerms 经 GUARDRAIL_BANNED_TERMS env 落地
- 软信号(jailbreak/developer mode/无限制…)→ 灰区,放行但打 safety_check 标志

Tier2(dispatcher harness LLM 分类器,escalation):
- 仅对灰区任务执行前调 LLM 裁决 jailbreak+severity,≥0.7 → rejected
- 明确干净/恶意的不付 LLM 成本;模型抖动/解析失败 fail-open 不误锁正常用户

契约新增 MetaSafetyCheck 透传灰区标志;orchestrator 加执行前护栏门控 + SetGuardian。
网关 6 单测 + dispatcher 4 单测,三模块全绿。live:拆字/base64/西里尔同形字均 422 拦,
恶意灰区被 LLM 拒(severity 1)、良性灰区(海盗 roleplay)放行完成。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-26 09:38:34 +08:00
parent 9506a82be9
commit 2f78fc565e
10 changed files with 395 additions and 35 deletions
@@ -25,11 +25,16 @@ func Guardrail() gin.HandlerFunc {
strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") {
// 限读上限 + 1 字节以判定"过大";命中拦截则后续 handler 不执行。
body, _ := io.ReadAll(io.LimitReader(c.Request.Body, guardrail.MaxJSONBytes+1))
if reason, blocked := guardrail.Inspect(body); blocked {
log.Printf("[guardrail] 拦截 %s %s%s", c.Request.Method, c.Request.URL.Path, reason)
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "输入护栏拦截:" + reason})
res := guardrail.Inspect(body)
if res.Blocked {
log.Printf("[guardrail] 拦截 %s %s%s", c.Request.Method, c.Request.URL.Path, res.Reason)
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "输入护栏拦截:" + res.Reason})
return
}
if res.Suspect { // 灰区:放行但打标,交 Dispatcher 的 LLM 分类器(Tier2)裁决
log.Printf("[guardrail] 灰区放行 %s %s:软信号 %v", c.Request.Method, c.Request.URL.Path, res.Signals)
c.Set("guardrail_suspect", true)
}
c.Request.Body = io.NopCloser(bytes.NewReader(body)) // 还原请求体供后续 handler 读取
}
c.Next()