Files
HomeAgent/internal/events/guard.go
JianFeeeee b850feaef2 feat: 段页式记忆流转、coldcontext 重命名与安全治理
记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边

命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据

安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据

验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
2026-10-29 23:53:48 +08:00

102 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package events
// 事件总线的**统一监控闸门**(类型与接口部分)。
//
// ## 为什么必须做在总线本体上,而不是各适配层
//
// 在此之前,授权只存在于 `internal/sdk/events.go`(插件走 `s.Events()` 那条路)。
// 而实际有**四条**路径能发出/收到事件:
//
// ① s.Events().Publish/Subscribe ← 过策略(唯一一条)
// ② s.Publish(evt) / s.Subscribe() ← 裸总线,不过策略
// ③ s.EventBus() 直接拿 *Bus ← 裸总线,连适配层都绕过
// ④ 内核模块 bus.Publish ← 内核自身,不需要策略
//
// 于是 `EventPolicy` 只是"合规者才受约束"的一纸约定:任何插件调
// `s.EventBus().Publish(...)` 就能发任意事件(含伪造内核事件)。
//
// 现在把闸门放到 **Bus.Publish / Bus.Subscribe 本身**(见 bus.go):
// 注册一个 Guard,所有发布与订阅都先问它。适配层不再持有策略,
// 只负责把「谁在调用」告诉总线。
//
// ## 为什么用接口注入而不是直接 import security
//
// events 是最底层的广播设施,security 依赖它(要发 security_ready)。
// 直接 import 会成环。所以这里定义**最小接口**,由装配方注入实现。
//
// 这与 syncbus 的「内核可写 / 插件只读」是同一思路:类型层面表达边界。
import "fmt"
// Publisher 是事件发布/订阅的主体身份。
//
// ★ 为什么必须显式传:没有身份就无法判定"这个 plugin 能不能发这个事件"。
// 空身份 = 内核内部(内核模块自己发布时不需要授权)。
type Publisher struct {
// Name 是插件名;空串表示**内核内部**。
//
// 内核模块(update / device / pluginhost / agent)发布系统事件时
// 不填 Name —— 它们本身就是可信的执行体,且没有"插件名"这个身份。
Name string
}
// IsKernel 报告该发布者是否内核内部。
func (p Publisher) IsKernel() bool { return p.Name == "" }
// Guard 是总线闸门:判定某个主体能否发布/订阅某事件。
//
// 实现在 internal/security(策略与载荷脱敏都在那里),
// events 包只负责"每次发布/订阅都问一遍"。
type Guard interface {
// AllowPublish 判定能否发布;allowed=false 时 reason 会进日志与错误。
AllowPublish(p Publisher, eventType EventType) (allowed bool, reason string)
// AllowSubscribe 判定能否订阅。
AllowSubscribe(p Publisher, eventType EventType) (allowed bool, reason string)
// ScrubPayload 对**出向**载荷做脱敏(凭据闸门)。
//
// 内核内部发布者同样要过:内核也可能把敏感值拼进事件
//(例如把 api_key 放进 Stage 事件里)。
ScrubPayload(eventType EventType, payload map[string]interface{}) map[string]interface{}
// ScrubInbound 对**入向**载荷做清洗(投递给订阅者之前)。
//
// ## 为什么入向必须在总线上做(用户 2026-10-29 指正)
//
// 原先入向清洗在 `internal/sdk/events.go` 的订阅回调里手工做(scrubIn),
// 而那条路径只盖住了 `s.Events().Subscribe` —— 其余路径
//(`s.EventBus()`、`s.Subscribe`、内核模块自己的 Subscribe)
// 拿到的都是**原始载荷**。同一个事件,订阅者看到的脏净程度取决于
// 它当初用的是哪个入口 —— 策略存在两份口径,迟早漂移。
//
// 安全模块既然持有总线、而**投递本身就在总线内部**,
// 入向清洗就该在那里做一次,而不是让每个适配层各做一遍。
//
// ## 与 ScrubPayload 的对称性
//
// 出向(发布时):发布者 → [ScrubPayload] → 总线
// 入向(投递时):总线 → [ScrubInbound] → 每个订阅者
//
// 两者都是“凭据闸门”,与身份无关:内核发出的载荷也要洗,
// 因为它同样可能拼进敏感值。
ScrubInbound(eventType EventType, payload map[string]interface{}) map[string]interface{}
}
// DeniedError 表示事件操作被闸门拒绝。
//
// ★ 为什么返回 error 而不是静默丢弃:静默丢弃会让插件以为
// "发了、没反应",那是最难排查的一类故障。
type DeniedError struct {
Publisher string
EventType string
Reason string
Subscribe bool
}
func (e *DeniedError) Error() string {
verb := "publish"
if e.Subscribe {
verb = "subscribe to"
}
return fmt.Sprintf("events: %q may not %s %q: %s", e.Publisher, verb, e.EventType, e.Reason)
}