fix(proc,qq)!: shm 模板 v2 —— fExtraBag 透传插件自建 Extra 键;qq 身份可序列化绑帧

内核 shm 桥 v2 前(见主仓同步提交)只透传 4 个具名 Extra 键,插件自建键
(qq 的 qq_auth 绑帧身份)在 stage 之间静默丢失 → before_toolcall 永远回落
可变全局身份 → 权限门绑帧在 proc 插件下整体失效(owner 被误拒/非 owner
可能被误放,2026-10-12 生产实证)。

模板(proc_main.go.tmpl):
- shmStageFieldCount 18→19、shmVersion 1→2(与内核镜像同步)
- fExtraBag:具名 4 键之外的自建键整体 JSON 装袋,read/snapshot/dirty 全链接入
- ReadInto:bag 先打平、具名键后覆盖(具名键永远赢,防御袋污染)

qq 插件:
- qqAuthContext 字段导出 + json tag:非导出字段过 JSON 桥会静默丢空
  (原根因链一环)
- authOnFrame 双形态:值形态(进程内/单测)+ map 形态(proc 桥往返,
  数字 float64);半截身份拒绝 —— 宁回落全局也不猜(owner 误判即提权)
- authFromMap 按 json tag 重建,逐字段校验类型

判据(plugin_test.go):往返无损/降权保持/半截拒绝(逐键)/帧身份优先于
全局(含 owner 被误拒的原始故障场景反向)。变异 Q1-Q4 全杀。
This commit is contained in:
JianFeeeee
2026-10-12 08:28:41 +08:00
parent 0b497f4876
commit 777c3a0ad3
3 changed files with 271 additions and 70 deletions

View File

@ -56,7 +56,7 @@ const (
// ---- StageContext 段内部布局(与内核 internal/plugin/proc/shm.go 一致)----
const (
shmStageFieldCount = 18
shmStageFieldCount = 19
shmSliceSize = 8
shmOffMagic = 0
@ -68,7 +68,7 @@ const (
shmOffSeq = 24
shmMagic = 0x48415348
shmVersion = 1
shmVersion = 2
)
// 字段索引(顺序须与内核 stageField 枚举一致)
@ -91,8 +91,35 @@ const (
fExtraMediaType
fExtraInputSource
fExtraOutputChannel
// fExtraBag:Extra 里具名 4 键之外的全部插件自建键(整体 JSON)。
// 插件可写:绑定帧态(如 qq_auth)就存这里;跨 stage 经内核回读存活。
fExtraBag
)
// shmNamedExtraKeys 是被提升为具名字段的 Extra 键;bag 打平时必须排除,
// 否则同一键会被具名字段与 bag 各带一份。
var shmNamedExtraKeys = map[string]bool{
"media_blocks": true, "media_type": true, "input_source": true, "output_channel": true,
}
// shmBagFromExtra 收集 Extra 里非具名键;无额外键返回 nil。
func shmBagFromExtra(extra map[string]interface{}) map[string]interface{} {
if len(extra) == 0 {
return nil
}
var bag map[string]interface{}
for k, v := range extra {
if shmNamedExtraKeys[k] {
continue
}
if bag == nil {
bag = make(map[string]interface{})
}
bag[k] = v
}
return bag
}
const (
flagNoMemory = 0
flagResponseSet = 1
@ -518,6 +545,15 @@ func readStageContext() (*sdk.StageContext, error) {
}
extra := map[string]interface{}{}
// bag 先打平,具名键随后覆盖 → 具名键永远赢(与内核 ReadInto 同构;
// 防御 bag 里混入具名键时用陈旧值顶掉具名字段)。
var bag map[string]interface{}
if err := unmarshalField(fExtraBag, &bag); err != nil {
return nil, err
}
for k, v := range bag {
extra[k] = v
}
for _, pair := range []struct {
field int
key string
@ -580,6 +616,8 @@ func takeStageSnapshot(sc *sdk.StageContext) *stageSnapshot {
sn.jsons[fMemory] = marshal(sc.Memory, len(sc.Memory))
sn.jsons[fTokenUsage] = marshal(sc.TokenUsage, len(sc.TokenUsage))
sn.jsons[fErrors] = marshal(sc.Errors, len(sc.Errors))
bag := shmBagFromExtra(sc.Extra)
sn.jsons[fExtraBag] = marshal(bag, len(bag))
if sc.Response != nil {
sn.response = *sc.Response