fix(proc)!: shm 桥 v2 —— fExtraBag 透传插件自建 Extra 键,修复绑桁权限门失效

根因(2026-10-12 生产实证):shm 桥此前只透传 4 个具名 Extra 键
(media_blocks/media_type/input_source/output_channel),插件自建键
(qq 的 qq_auth 绑帧身份)在 WriteDirty/captureLocal 处被静默丢弃,
下一 stage ReadInto 重建的 Extra 永远没有它 → authOnFrame 恒 miss →
before_toolcall 回落到可变全局身份。owner 轮被并发的非 owner 中断
合并降权后,自己的 memory_commit 被硬私名单误拒(日志 22:20:58
「改写了 1 个字段」);反向时序则可能把非 owner 误放为 owner。

- shm.go:stageField 加 fExtraBag(19 字段),shmVersion 1→2
  (AttachSegment 与插件握手对旧版失败即拒,不静默错读新布局)
- shmcodec.go:captureLocal/WriteAll/ReadInto/newSnapshotFromLocal
  全链接入 bag;写侧过滤具名键(bag 不偷具名键槽位);读侧 bag 先
  打平、具名键后覆盖(具名键永远赢);不可序列化值显式报错——
  宁失败不静默丢键(丢键对 qq 是权限失效)
- extra_bag_test.go:判据 8 条(往返无损/具名键语义不变/不可序列化
  报错/WriteDirty 零写与脏写/跨 stage 帧存活/袋污染具名键必输/
  raw-JSON 写侧过滤/快照脏检),变异 M1-M3 全杀

配套:SDK 仓 777c3a0(模板 v2 同步 + qq 身份可序列化 + 插件侧判据)。
部署注意:shmVersion 2 后全部 23 个 proc 插件须用新 hmapdev 重编。
This commit is contained in:
JianFeeeee
2026-10-12 08:30:14 +08:00
parent b15994d964
commit 6abf03bd78
3 changed files with 397 additions and 2 deletions

View File

@ -66,6 +66,36 @@ type localCtx struct {
ExtraMediaType interface{}
ExtraInputSource interface{}
ExtraOutputChan interface{}
// ExtraBag 是 Extra 里具名 4 键之外的全部键(插件自建,如 qq_auth)。
// nil 表示无额外键;非 nil 时整体 JSON 进 fExtraBag。
ExtraBag map[string]interface{}
}
// namedExtraKeys 是被提升为具名字段的 4 个 Extra 键。bag 必须排除它们,
// 否则同一键会被具名字段与 bag 各带一份,回读时语义冲突。
var namedExtraKeys = map[string]bool{
ExtraKeyMediaBlocks: true,
ExtraKeyMediaType: true,
ExtraKeyInputSource: true,
ExtraKeyOutputChannel: true,
}
// bagFromExtra 把 Extra 里非具名键收集成 bag(无额外键时返回 nil)。
func bagFromExtra(extra map[string]interface{}) map[string]interface{} {
if len(extra) == 0 {
return nil
}
var bag map[string]interface{}
for k, v := range extra {
if namedExtraKeys[k] {
continue
}
if bag == nil {
bag = make(map[string]interface{})
}
bag[k] = v
}
return bag
}
func captureLocal(sc *pubsdk.StageContext) *localCtx {
@ -91,6 +121,7 @@ func captureLocal(sc *pubsdk.StageContext) *localCtx {
l.ExtraMediaType = sc.Extra[ExtraKeyMediaType]
l.ExtraInputSource = sc.Extra[ExtraKeyInputSource]
l.ExtraOutputChan = sc.Extra[ExtraKeyOutputChannel]
l.ExtraBag = bagFromExtra(sc.Extra)
}
return l
}
@ -166,6 +197,9 @@ func (s *Segment) writeLocal(l *localCtx) error {
{fExtraMediaType, l.ExtraMediaType},
{fExtraInputSource, l.ExtraInputSource},
{fExtraOutputChannel, l.ExtraOutputChan},
// bag 空时写成未设置(sliceOrNil);非空时整体 marshal。
// 不可序列化的值在这里让 putJSON 显式报错(宁失败不静默丢键)。
{fExtraBag, sliceOrNil(len(l.ExtraBag), l.ExtraBag)},
}
for _, step := range jsonFields {
if err := putJSON(step.f, step.v); err != nil {
@ -263,6 +297,16 @@ func (s *Segment) ReadInto(sc *pubsdk.StageContext) error {
}
extra := map[string]interface{}{}
// bag 先打平,具名键随后覆盖 → 具名键永远赢。
// 顺序是防御:即便 bag 里混入具名键(写侧 bug/旧数据),
// 也不会用 bag 的陈旧值顶掉具名字段的新值。
var bag map[string]interface{}
if err := getJSON(fExtraBag, &bag); err != nil {
return err
}
for k, v := range bag {
extra[k] = v
}
for _, pair := range []struct {
f stageField
key string
@ -281,6 +325,7 @@ func (s *Segment) ReadInto(sc *pubsdk.StageContext) error {
}
}
var respPtr *string
if s.getFlag(flagResponseSet) {
r, err := getStr(fResponse)
@ -461,6 +506,7 @@ func newSnapshotFromLocal(l *localCtx) *Snapshot {
sn.jsons[fExtraMediaType] = marshal(l.ExtraMediaType)
sn.jsons[fExtraInputSource] = marshal(l.ExtraInputSource)
sn.jsons[fExtraOutputChannel] = marshal(l.ExtraOutputChan)
sn.jsons[fExtraBag] = marshal(sliceOrNil(len(l.ExtraBag), l.ExtraBag))
if l.Response != nil {
sn.response = *l.Response
@ -509,6 +555,8 @@ func (f stageField) String() string {
return "extra." + ExtraKeyInputSource
case fExtraOutputChannel:
return "extra." + ExtraKeyOutputChannel
case fExtraBag:
return "extra.bag"
}
return fmt.Sprintf("field(%d)", int(f))
}