// Package engine - secret_store.go 实现凭据注册表. // // # 问题背景 // // Agent 执行工具时常需要访问外部系统(数据库,API,云服务), // 这些凭据(密码,Token,API Key)不应出现在: // - AI 看到的工具输出中(防止模型在后续提示词中泄露) // - 引擎推送给消费层的 Event 流中(防止日志/监控系统记录明文凭据) // - 内存文件中(SecretGuard 已覆盖这一层) // // # 设计决策 // // SecretStore 只做两件事: // 1. 向 Bash 工具子进程注入 env var(工具通过 $NAME 引用凭据) // 2. 对所有工具输出做 value-level 脱敏(将明文替换为 [SECRET:name]) // // 升华改进(ELEVATED): 早期实现 只有静态的 GHA_SUBPROCESS_SCRUB 白名单, // 只覆盖 CI 环境的已知 env var 名,无法处理用户自定义凭据. // 我们允许用户动态注册任意 secret,引擎自动在全部工具输出中脱敏. // 替代方案:<要求调用方自己过滤 EventChannel 中的输出> // - 否决:消费层无法感知所有输出路径(工具结果被送回模型的路径尤其隐蔽). package engine import ( "fmt" "strings" "sync" ) // minSecretLen 是 secret value 的最小字节数. // // 精妙之处(CLEVER): 强制最小长度 8 字节-- // 过短的 value(如单字符 "x")会导致大量误替换(正常输出中的 "x" 全被脱敏). // 8 字节是工程妥协点:足够防止误替换,同时不妨碍常见凭据格式(UUIDs/tokens 通常 ≥16 字节). // 调用方应确保 secret 足够长;若 secret 真的 <8 字节,应在业务层决定是否允许,而非静默放行. const minSecretLen = 8 // SecretStore 是线程安全的凭据注册表. // // 设计约束: // - value 不对外暴露(无 Get(name) 方法),防止调用方误写日志 // - Redact() 只替换,不缓存替换结果(输入随 tool output 变化,缓存无意义) // - Environ() 返回的 slice 是快照,调用方修改不影响 store type SecretStore struct { mu sync.RWMutex secrets map[string]string // name → value(原始值,不对外暴露) order []string // 注册顺序,保证 Redact/Environ 结果稳定 } // newSecretStore 创建空的 SecretStore. func newSecretStore() *SecretStore { return &SecretStore{ secrets: make(map[string]string), } } // Add 注册一个 secret. // // name 用作 env var 名称(建议大写,如 "DB_PASSWORD")和脱敏标签([SECRET:DB_PASSWORD]). // value 不得短于 minSecretLen 字节(防止超短 value 引发大量误替换). // // 同名 secret 允许覆盖(支持 token rotation / re-roll 场景). // 返回 error 而非 panic,让调用方决定如何处理(测试或 CLI 可 log.Fatal,SDK 可返回给调用方). func (s *SecretStore) Add(name, value string) error { if len(value) < minSecretLen { return fmt.Errorf( "secret %q value too short (min %d bytes): got %d", name, minSecretLen, len(value), ) } s.mu.Lock() defer s.mu.Unlock() if _, exists := s.secrets[name]; !exists { s.order = append(s.order, name) } s.secrets[name] = value return nil } // Redact 将字符串中所有已注册 secret 的 value 替换为 [SECRET:name]. // // 精妙之处(CLEVER): 按注册顺序替换,而非按 value 长度降序-- // 简化实现,且实践中 secret value 几乎不会互相包含(短 value 被最小长度约束过滤). // 若确实需要"长 value 优先"(防止子串被短 value 替换后长 value 匹配失败), // 调用方可以在注册时控制顺序,或使用带前缀的 secret name 区分. // // 替代方案: // - 否决:secret 数量通常 <20,strings.ReplaceAll 性能足够,无需引入复杂算法. func (s *SecretStore) Redact(input string) string { s.mu.RLock() defer s.mu.RUnlock() out := input for _, name := range s.order { val := s.secrets[name] if val != "" && strings.Contains(out, val) { out = strings.ReplaceAll(out, val, "[SECRET:"+name+"]") } } return out } // Environ 返回所有 secret 以 "NAME=VALUE" 格式的 env var slice. // // 用途:注入 Bash 工具子进程环境变量,让工具脚本能通过 $NAME 引用凭据, // 同时不需要调用方手动管理 env var 列表. // // 返回的 slice 是快照,调用方修改不影响 store. func (s *SecretStore) Environ() []string { s.mu.RLock() defer s.mu.RUnlock() envs := make([]string, 0, len(s.secrets)) for _, name := range s.order { envs = append(envs, name+"="+s.secrets[name]) } return envs } // Len 返回已注册的 secret 数量. // 主要用于测试断言和监控(不暴露具体 name/value). func (s *SecretStore) Len() int { s.mu.RLock() defer s.mu.RUnlock() return len(s.secrets) } // merge 创建一个新 SecretStore,包含 base 的所有 secret 加上 extras. // extras 中的同名 secret 会覆盖 base 中的值. // // 精妙之处(CLEVER): 不修改 base,而是创建新实例-- // engine-level secrets(base)是多请求共享的;per-request secrets(extras)仅对本请求有效. // 合并到新实例后,旧 base 不受影响,并发安全. func mergeSecrets(base *SecretStore, extras []secretEntry) *SecretStore { merged := newSecretStore() // 先复制 base base.mu.RLock() for _, name := range base.order { merged.secrets[name] = base.secrets[name] merged.order = append(merged.order, name) } base.mu.RUnlock() // 再叠加 per-request extras(同名覆盖) for _, e := range extras { if _, exists := merged.secrets[e.name]; !exists { merged.order = append(merged.order, e.name) } merged.secrets[e.name] = e.value } return merged } // secretEntry 是 per-request secret 的内部表示. // 存放在 runConfig 中,合并时转入临时 SecretStore. type secretEntry struct { name string value string }