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:
@@ -75,6 +75,15 @@ func (o *Orchestrator) runComposeConversation(ctx context.Context, taskID string
|
||||
|
||||
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 {
|
||||
@@ -87,11 +96,9 @@ func (o *Orchestrator) runComposeConversation(ctx context.Context, taskID string
|
||||
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.info(node, "system", "compose 图", fmt.Sprintf("%d 段输出 / %d 字(Eino compose 运行时)", chunks, len([]rune(produced.String()))))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user