mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
feat(sdk): 受限事件总线的内核实现 + clawhubadapter 改用它
- internal/sdk/events.go:实现 pubsdk.EventsAPI
· publishAllowed:白名单精确匹配 或 本插件命名空间 plugin.<name>.<后缀>
· subscribeAllowed:只开只读事件(tools_ready/security_ready/device_ready)
· 载荷出向经 security.PrepareOutbound(明文→@{别名}),入向经 ScrubInbound
· 键数上限 64;越界**报错**不静默
· Source 由 SDK 固定成插件名(插件不能冒名)
- PluginSDK.Events() 访问器
- clawhubadapter 的 skill_detected 改走 s.Events().Publish,不再 import internal/events
⇒ 它为外部化的唯一阻塞项(事件发布)已消除
判据(internal/sdk/events_test.go,6 条):
白名单放行 / 越界拒绝(raw_input、agent_output、update_request、stage、system、
别的插件命名空间、空类型)/ 自己命名空间放行 / 订阅越界拒绝 / 载荷凭据被替换成别名 / 键数上限
This commit is contained in:
@ -1,6 +1,13 @@
|
||||
package sdk
|
||||
|
||||
import "github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||||
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||||
)
|
||||
|
||||
// 事件类型/事件类型别名:插件经 SDK 订阅/发布内核事件,
|
||||
// 无需直接 import internal/events(内核事件总线仅通过 SDK 暴露)。
|
||||
@ -30,3 +37,153 @@ const (
|
||||
// 实体名是中文概念(实测命中 0),只能退化成点亮一批固定节点。
|
||||
EventMemoryAccess = events.EventMemoryAccess
|
||||
)
|
||||
|
||||
// ───────────────────── 受限事件总线(插件可见) ─────────────────────
|
||||
//
|
||||
// 边界由三件事决定,缺一不可:
|
||||
// 1. 类型白名单 + 插件自己的命名空间(`plugin.<name>.*`)
|
||||
// 2. 载荷出向经 security 审查(明文凭据换成 @{别名})+ 键数上限
|
||||
// 3. 订阅只开只读事件(tools_ready / security_ready / device_ready)
|
||||
//
|
||||
// 为什么不直接把 *events.Bus 交给插件:Bus.Publish 能发 EventAll 通配类型、
|
||||
// 能伪造任意 Source、载荷不做任何审查。那不是"多给一个便利",而是把内核
|
||||
// 内部同步面变成没有边界的公共写口。
|
||||
|
||||
const (
|
||||
// eventPayloadMaxKeys 限制载荷顶层键数,防止"把一个对象当事件"塞进来。
|
||||
// 事件是**通知**,不是数据通道:大块数据该走插件自己的通道或工具返回值。
|
||||
eventPayloadMaxKeys = 64
|
||||
)
|
||||
|
||||
type eventsAPI struct {
|
||||
pluginName string
|
||||
bus *events.Bus
|
||||
sec *security.Runtime // 可 nil(安全模块未装配时零成本跳过)
|
||||
unsubs []func()
|
||||
}
|
||||
|
||||
// newEventsAPI 构造插件的受限事件面(bus 为 nil 时返回 nil,调用方按 nil 处理)。
|
||||
func newEventsAPI(pluginName string, bus *events.Bus) pubsdk.EventsAPI {
|
||||
if bus == nil {
|
||||
return nil
|
||||
}
|
||||
return &eventsAPI{pluginName: pluginName, bus: bus, sec: security.RuntimeGlobal()}
|
||||
}
|
||||
|
||||
func (e *eventsAPI) AllowedPublish() []string {
|
||||
out := pubsdk.PluginPublishableEvents()
|
||||
return append(out, "plugin."+e.pluginName+".*")
|
||||
}
|
||||
|
||||
func (e *eventsAPI) AllowedSubscribe() []string { return pubsdk.PluginSubscribableEvents() }
|
||||
|
||||
// publishAllowed:白名单精确匹配,或本插件命名空间前缀(plugin.<name>.<后缀>)。
|
||||
func (e *eventsAPI) publishAllowed(t string) bool {
|
||||
for _, a := range pubsdk.PluginPublishableEvents() {
|
||||
if t == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
prefix := "plugin." + e.pluginName + "."
|
||||
return strings.HasPrefix(t, prefix) && len(t) > len(prefix)
|
||||
}
|
||||
|
||||
func (e *eventsAPI) subscribeAllowed(t string) bool {
|
||||
for _, a := range pubsdk.PluginSubscribableEvents() {
|
||||
if t == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *eventsAPI) Publish(eventType string, payload map[string]interface{}) error {
|
||||
if eventType == "" {
|
||||
return fmt.Errorf("events: event type must not be empty")
|
||||
}
|
||||
if !e.publishAllowed(eventType) {
|
||||
return fmt.Errorf("events: plugin %q may not publish %q (allowed: %v)",
|
||||
e.pluginName, eventType, e.AllowedPublish())
|
||||
}
|
||||
if payload == nil {
|
||||
payload = map[string]interface{}{}
|
||||
}
|
||||
if len(payload) > eventPayloadMaxKeys {
|
||||
return fmt.Errorf("events: payload has %d keys, limit is %d", len(payload), eventPayloadMaxKeys)
|
||||
}
|
||||
|
||||
// ★ 载荷出向审查:事件载荷会被其他订阅者(含 WebUI/状态面)读到,
|
||||
// 插件顺手把凭据放进去就等于绕过凭据闸门。
|
||||
e.bus.Publish(&events.Event{
|
||||
Type: events.EventType(eventType),
|
||||
Source: e.pluginName,
|
||||
Payload: e.scrubOut(eventType, payload),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// scrubOut 对载荷做一次出向脱敏(安全模块未装配时原样返回)。
|
||||
func (e *eventsAPI) scrubOut(eventType string, payload map[string]interface{}) map[string]interface{} {
|
||||
if e.sec == nil {
|
||||
return payload
|
||||
}
|
||||
out := make(map[string]interface{}, len(payload))
|
||||
for k, v := range payload {
|
||||
if s, ok := v.(string); ok {
|
||||
if clean, n := e.sec.PrepareOutbound(security.Inspection{
|
||||
Direction: security.DirectionOutbound,
|
||||
Stage: "event:" + eventType,
|
||||
Source: e.pluginName,
|
||||
}, s); n > 0 {
|
||||
out[k] = clean
|
||||
continue
|
||||
}
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *eventsAPI) Subscribe(eventType string, handler func(map[string]interface{})) (func(), error) {
|
||||
if !e.subscribeAllowed(eventType) {
|
||||
return nil, fmt.Errorf("events: plugin %q may not subscribe to %q (allowed: %v)",
|
||||
e.pluginName, eventType, e.AllowedSubscribe())
|
||||
}
|
||||
if handler == nil {
|
||||
return nil, fmt.Errorf("events: handler must not be nil")
|
||||
}
|
||||
unsub := e.bus.Subscribe(events.EventType(eventType), func(evt *events.Event) {
|
||||
handler(e.scrubIn(eventType, evt.Payload))
|
||||
})
|
||||
e.unsubs = append(e.unsubs, unsub)
|
||||
return unsub, nil
|
||||
}
|
||||
|
||||
// scrubIn 对入向载荷做清洗(事件可能带其他模块写进来的内容)。
|
||||
func (e *eventsAPI) scrubIn(eventType string, p map[string]interface{}) map[string]interface{} {
|
||||
if e.sec == nil || p == nil {
|
||||
return p
|
||||
}
|
||||
out := make(map[string]interface{}, len(p))
|
||||
for k, v := range p {
|
||||
if s, ok := v.(string); ok {
|
||||
if clean, n := e.sec.ScrubInbound(security.Inspection{
|
||||
Direction: security.DirectionInbound,
|
||||
Stage: "event:" + eventType,
|
||||
}, s); n > 0 {
|
||||
out[k] = clean
|
||||
continue
|
||||
}
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// stop 解除本插件建立的全部订阅(插件卸载时由宿主调用)。
|
||||
func (e *eventsAPI) stop() {
|
||||
for _, u := range e.unsubs {
|
||||
u()
|
||||
}
|
||||
e.unsubs = nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user