feat(harness): 输出脱敏增强 —— 跨分片 StreamRedactor + PII,杜绝密钥碎片泄漏
原逐片脱敏有两个漏:①密钥被切成两片("sk-912cf85b"|"16d0...")逐片都不命中正则而漏检; ②贪婪正则在缓冲末尾凑够最短长度就把半截密钥提前脱敏发走、剩余字符随后明文流出(碎片泄漏)。 有状态 StreamRedactor 跨分片缓冲,切点在「原文」上定且绝不切断任何完整匹配: - opener 暂留末尾仍在增长的疑似密钥(sk/AKIA/JWT/Bearer/手机/邮箱/长数字) - 始终留 16B 尾窗兜底 opener 未覆盖的短模式;勿切断完整匹配(循环至稳定) - rune 边界安全:cut 退到最近 rune 起点,中文不被切成半个发出乱码 - 暂留封顶 256B,防对抗性长串无限暂留 / O(n²) - 新增 PII:手机号 / 邮箱 / 身份证(18 位) 3 个流式点(graph/react_agent/compose_graph)统一接入,逐片 Push + 收尾 Flush。 7 单测(跨片/逐字符 JWT/碎片回归/尾窗内匹配/干净重建/PII/无误伤),-race 干净。 live 实测:26 位密钥(曾泄漏 ijkl90mnop 碎片)与邮箱整条 [已脱敏]。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -214,6 +214,15 @@ func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *boar
|
||||
|
||||
chunks := 0
|
||||
var produced strings.Builder // 本节点产出(供下游 agent 接力)
|
||||
red := harness.NewStreamRedactor() // 输出护栏:跨分片脱敏,杜绝密钥被切断而漏检
|
||||
emit := func(safe string) {
|
||||
if safe == "" {
|
||||
return
|
||||
}
|
||||
_ = o.sink.PublishToken(taskID, []byte(safe))
|
||||
produced.WriteString(safe)
|
||||
chunks++
|
||||
}
|
||||
for {
|
||||
chunk, rerr := sr.Recv()
|
||||
if rerr == io.EOF {
|
||||
@@ -226,11 +235,9 @@ func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *boar
|
||||
if chunk.Content == "" {
|
||||
continue // 工具调用片段无正文,跳过;正文只来自模型答复
|
||||
}
|
||||
safe, _ := harness.RedactSecrets(chunk.Content)
|
||||
_ = o.sink.PublishToken(taskID, []byte(safe))
|
||||
produced.WriteString(safe)
|
||||
chunks++
|
||||
emit(red.Push(chunk.Content))
|
||||
}
|
||||
emit(red.Flush()) // 吐出暂留尾部
|
||||
o.recordAgentOutput(b, produced.String())
|
||||
tr.emit(node, "model", "end", "ReAct 智能体",
|
||||
fmt.Sprintf("%d 段输出 / %d 字", chunks, len([]rune(produced.String()))), time.Since(t0).Milliseconds())
|
||||
|
||||
Reference in New Issue
Block a user