执行方案 DevDocs/audit/_plan.md 阶段 0 · G0-B:该文件距 500 行红线仅 29 行,而后续
11.4.2(memory/guardrail/orchestration 三类 span 生产者)等改动必然推进它 ⇒ 先拆。
- graph.go(159):对外类型与快照 API(FlowStatus* / FlowNode / FlowEdge / FlowGraph /
NodeSpec / RunGraph;NewRunGraph / Start / SeedInput / SeedOutput / Snapshot)
- graph_build.go(245):构图与折叠(Upsert / rememberParent / laneOf / merge / addEdge /
AddSpan / AddSpanStart / kindForSpan / labelForSpan / statusForFinished)
- graph_props.go(77):prop 白名单与脱敏(safeSpanProps / safePropKeys / summarize / truncate)
纯搬移机械证明:拆前与拆后(取非空、非 package、非 import 块行)排序后 **md5 相同、
diff 为空**;声明集合 28 条守恒;唯一新增文本 = 两个新文件的 package 行与 import 块。
`safePropKeys`(含 4bf1f7c 刚加入的 "persona")原样落在 graph_props.go,逐字节未动。
验证:go build/vet 绿;internal/runtime/trace + internal/services/gateway 测试绿(含
graph_persona_test 与 flow_update_ws_test 的 graph_update 线上帧成对契约回归);
linux/amd64 + darwin/arm64 交叉编译绿;三文件 159/245/77 行均 ≤500;gofmt -l 无输出。
78 lines
2.5 KiB
Go
78 lines
2.5 KiB
Go
package trace
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
)
|
||
|
||
// maxSummaryRunes bounds how many runes of a user string / tool result are
|
||
// rendered into a node label or prop summary, so sensitive or long content is
|
||
// collapsed to a safe, dashboard-friendly prefix.
|
||
const maxSummaryRunes = 80
|
||
|
||
// safeSpanProps carries only a whitelist of span props to the frontend, plus a
|
||
// desensitized tool result summary. It never forwards raw arguments or secrets, and
|
||
// every string is scrubbed of credential shapes and truncated, so long or sensitive
|
||
// content cannot leak a full payload (design/19 §3).
|
||
func safeSpanProps(rec SpanRecord) map[string]any {
|
||
var props map[string]any
|
||
for _, k := range safePropKeys {
|
||
v, ok := rec.Props[k]
|
||
if !ok {
|
||
continue
|
||
}
|
||
if props == nil {
|
||
props = make(map[string]any)
|
||
}
|
||
props[k] = redactValue(v)
|
||
}
|
||
if rec.Kind == SpanKindTool {
|
||
if v, ok := rec.Props["result"]; ok {
|
||
if props == nil {
|
||
props = make(map[string]any)
|
||
}
|
||
props["result_summary"] = RedactText(summarize(v))
|
||
}
|
||
}
|
||
return props
|
||
}
|
||
|
||
// safePropKeys are the span prop keys forwarded to a node's props.
|
||
//
|
||
// persona 是子代理分支根节点上的「目标角色 id」:orchestration/scheduler.go 只在
|
||
// subagent_spawn 显式指定角色时才设置它,且该 id 必须先通过角色注册表校验
|
||
// (ErrUnknownPersona 会拒绝未注册的 id,orchestration/persona.go),因此它是注册表里
|
||
// 的角色标识,不是用户内容、不是提示词、更不是凭据——同一个 id 本来就随 GET /personas
|
||
// 与会话材料对外可见。它是 Docs/api-client.md §3.5(v0.47)对客户端的承诺
|
||
// (design/22 §8-2:让流程图显示「这个子代理是以谁的身份在替班」),漏出白名单就会被
|
||
// safeSpanProps 静默丢掉。未指定角色派发时该键不存在,符合 v0.47「纯增量」的约定。
|
||
var safePropKeys = []string{"stop_reason", "step", "error", "error_kind", "tool", "persona"}
|
||
|
||
// summarize renders an arbitrary value as a short text string for a prop summary.
|
||
func summarize(v any) string {
|
||
switch x := v.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return x
|
||
default:
|
||
b, err := json.Marshal(x)
|
||
if err != nil {
|
||
return fmt.Sprintf("%v", x)
|
||
}
|
||
return string(b)
|
||
}
|
||
}
|
||
|
||
// truncate cuts s to at most max runes, appending an ellipsis when it overflows.
|
||
func truncate(s string, max int) string {
|
||
if max <= 0 {
|
||
return s
|
||
}
|
||
r := []rune(s)
|
||
if len(r) <= max {
|
||
return s
|
||
}
|
||
return string(r[:max]) + "…"
|
||
}
|