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
@@ -85,9 +85,10 @@ type Orchestrator struct {
sink TokenSink
tools ToolCaller
exec ExecSink
status StatusSink // 任务生命周期状态回写(可为 nil)
approval ApprovalWaiter // HITL 审批等待(可为 nil → 审批节点自动放行)
evalSink EvalSink // 评测结果回写落库(可为 nil → 仅打日志)
status StatusSink // 任务生命周期状态回写(可为 nil)
approval ApprovalWaiter // HITL 审批等待(可为 nil → 审批节点自动放行)
evalSink EvalSink // 评测结果回写落库(可为 nil → 仅打日志)
guard *harness.Classifier // 输入护栏 Tier2:对网关标记的灰区任务做 LLM 裁决(可为 nil → 不做)
turnMu sync.Mutex // 保护 turns(攒批计数,多任务 goroutine 共享)
turns map[string]int // sessionID → 累计轮次,用于每 N 轮触发 consolidate
@@ -100,6 +101,9 @@ func NewOrchestrator(pool LLM, breaker *harness.CircuitBreaker, eval *harness.Ev
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec, status: status, approval: approval, evalSink: evalSink}, nil
}
// SetGuardian 注入输入护栏 Tier2 的 LLM 分类器(可选;不注入则灰区任务直接放行执行)。
func (o *Orchestrator) SetGuardian(c *harness.Classifier) { o.guard = c }
// setStatus 回写一次任务状态流转(status 为 nil 时静默跳过)。
func (o *Orchestrator) setStatus(taskID, status, detail string) {
if o.status == nil {
@@ -148,6 +152,29 @@ func (o *Orchestrator) Handle(ctx context.Context, t *contract.Task) error {
return nil
}
// 输入护栏 Tier2:仅对网关 Tier1 标记的「灰区」任务做 LLM 越狱裁决(明确干净/恶意的已在网关处理,
// 不付 LLM 成本)。命中即拒绝执行(合法终态,非故障,不计熔断)。
if o.guard != nil {
if safety, _ := t.Meta[contract.MetaSafetyCheck].(bool); safety {
query := dsl.Compile(t.Graph).Query
gctx, gcancel := context.WithTimeout(ctx, 10*time.Second)
block, sev, reason := o.guard.Classify(gctx, query)
gcancel()
if block {
slog.WarnContext(ctx, "input guardrail blocked", "task_id", t.ID, "severity", sev, "reason", reason)
tr.info("guardrail", "system", "输入护栏拦截", reason)
_ = o.sink.PublishToken(t.ID, []byte("⚠️ 输入被安全护栏拦截:"+reason))
_ = o.sink.CompleteStream(t.ID)
o.breaker.Report(true) // 安全拦截是策略决策,非后端故障
o.setStatus(t.ID, contract.TaskRejected, truncate("输入护栏:"+reason, 200))
return nil
}
if sev > 0 {
slog.InfoContext(ctx, "input guardrail suspect passed", "task_id", t.ID, "severity", sev)
}
}
}
// 任务状态机:进入执行 → running;整体加超时上限,超时判 timeout(杜绝无限期"运行中")。
o.setStatus(t.ID, contract.TaskRunning, "")
tctx, cancel := context.WithTimeout(ctx, taskExecTimeout)