mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 14:27:30 +00:00
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:
@ -85,14 +85,58 @@ type DownloadTask struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// qqAuthContext 是绑帧身份。字段导出 + json tag:proc 插件下身份要经
|
||||
// 共享段 JSON 往返(内核 shm 桥 v2 的 fExtraBag),非导出字段会在
|
||||
// marshal 时静默丢失 —— 那正是 2026-10-12 权限门失效的根因链一环。
|
||||
type qqAuthContext struct {
|
||||
active bool
|
||||
owner bool
|
||||
messageID int64
|
||||
userID int64
|
||||
groupID int64
|
||||
isGroup bool
|
||||
generation uint64
|
||||
Active bool `json:"active"`
|
||||
Owner bool `json:"owner"`
|
||||
MessageID int64 `json:"message_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
IsGroup bool `json:"is_group"`
|
||||
Generation uint64 `json:"generation"`
|
||||
}
|
||||
|
||||
// authFromMap 按 JSON 往返约定重建身份(数字统一 float64)。
|
||||
// 解不出完整身份(键缺失/类型错)返回 false —— 宁回落全局也不猜:
|
||||
// 半截的身份比没有更危险(owner 误判即提权)。
|
||||
func authFromMap(m map[string]interface{}) (qqAuthContext, bool) {
|
||||
var a qqAuthContext
|
||||
getBool := func(k string) (bool, bool) {
|
||||
v, ok := m[k].(bool)
|
||||
return v, ok
|
||||
}
|
||||
getInt := func(k string) (int64, bool) {
|
||||
f, ok := m[k].(float64)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return int64(f), true
|
||||
}
|
||||
var ok bool
|
||||
if a.Active, ok = getBool("active"); !ok {
|
||||
return a, false
|
||||
}
|
||||
if a.Owner, ok = getBool("owner"); !ok {
|
||||
return a, false
|
||||
}
|
||||
if a.MessageID, ok = getInt("message_id"); !ok {
|
||||
return a, false
|
||||
}
|
||||
if a.UserID, ok = getInt("user_id"); !ok {
|
||||
return a, false
|
||||
}
|
||||
if a.GroupID, ok = getInt("group_id"); !ok {
|
||||
return a, false
|
||||
}
|
||||
if a.IsGroup, ok = getBool("is_group"); !ok {
|
||||
return a, false
|
||||
}
|
||||
if g, present := m["generation"].(float64); present {
|
||||
a.Generation = uint64(g)
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
var qqMessageIDRe = regexp.MustCompile(`message_id=(-?\d+)`)
|
||||
@ -982,7 +1026,7 @@ func argInt64(args map[string]interface{}, key string) (int64, bool) {
|
||||
}
|
||||
|
||||
func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}, auth qqAuthContext) (bool, string) {
|
||||
if !auth.active || auth.owner {
|
||||
if !auth.Active || auth.Owner {
|
||||
return true, ""
|
||||
}
|
||||
// 输出工具**不受"当前会话"身份限制**(先于身份判据返回)。
|
||||
@ -998,9 +1042,9 @@ func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
currentPeer := auth.userID
|
||||
if auth.isGroup {
|
||||
currentPeer = auth.groupID
|
||||
currentPeer := auth.UserID
|
||||
if auth.IsGroup {
|
||||
currentPeer = auth.GroupID
|
||||
}
|
||||
if currentPeer == 0 {
|
||||
return false, "可信 QQ 会话身份不完整"
|
||||
@ -1008,10 +1052,10 @@ func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}
|
||||
matchCurrentPeer := func() bool {
|
||||
groupID, hasGroup := argInt64(args, "group_id")
|
||||
userID, hasUser := argInt64(args, "user_id")
|
||||
if auth.isGroup {
|
||||
return hasGroup && groupID == auth.groupID && !hasUser
|
||||
if auth.IsGroup {
|
||||
return hasGroup && groupID == auth.GroupID && !hasUser
|
||||
}
|
||||
return hasUser && userID == auth.userID && !hasGroup
|
||||
return hasUser && userID == auth.UserID && !hasGroup
|
||||
}
|
||||
|
||||
switch name {
|
||||
@ -1024,16 +1068,16 @@ func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}
|
||||
if !ok {
|
||||
return false, "缺少有效 message_id"
|
||||
}
|
||||
if messageID == auth.messageID {
|
||||
if messageID == auth.MessageID {
|
||||
return true, ""
|
||||
}
|
||||
peerID, isGroup, _, found := p.lookupMsgRef(messageID)
|
||||
if !found || isGroup != auth.isGroup || peerID != currentPeer {
|
||||
if !found || isGroup != auth.IsGroup || peerID != currentPeer {
|
||||
return false, "message_id 不属于当前 QQ 会话"
|
||||
}
|
||||
case p.name + "_get_group_member_info", p.name + "_get_group_files":
|
||||
groupID, ok := argInt64(args, "group_id")
|
||||
if !auth.isGroup || !ok || groupID != auth.groupID {
|
||||
if !auth.IsGroup || !ok || groupID != auth.GroupID {
|
||||
return false, "只能访问当前 QQ 群的数据"
|
||||
}
|
||||
}
|
||||
@ -1046,7 +1090,7 @@ func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}
|
||||
// - 中断会抢占当前轮并把现场压栈(scheduler 的 suspendStack),中断轮收尾时
|
||||
// afterOutput 把插件全局身份清空;随后外层被恢复(resumeTask 复用同一帧、
|
||||
// 不重跑 StageOnInput),若身份只存全局,恢复后的外层就是"无身份"——
|
||||
// beforeToolcall 会在 !auth.active 处直接返回,权限门整体失效。
|
||||
// beforeToolcall 会在 !auth.Active 处直接返回,权限门整体失效。
|
||||
// - 运行中到达的新消息会调 activateAuthContext 改写全局身份,把**正在跑的那一轮**
|
||||
// 换成另一方的身份(换高=越权,换低=误拒)。
|
||||
//
|
||||
@ -1054,13 +1098,22 @@ func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}
|
||||
const qqAuthExtraKey = "qq_auth"
|
||||
|
||||
// authOnFrame 读取本帧绑定的身份;ok=false 表示本帧未绑定过 QQ 身份。
|
||||
// 调用方需持有 ctx 的读(或写)锁。
|
||||
//
|
||||
// 双形态兼容(proc 桥 JSON 往返约定):
|
||||
// - 值形态(进程内 stage / 单测):类型断言直接命中;
|
||||
// - map 形态(proc 插件:身份经共享段 JSON 往返):按 json tag 重建。
|
||||
func authOnFrame(ctx *sdk.StageContext) (qqAuthContext, bool) {
|
||||
if ctx == nil || ctx.Extra == nil {
|
||||
return qqAuthContext{}, false
|
||||
}
|
||||
auth, ok := ctx.Extra[qqAuthExtraKey].(qqAuthContext)
|
||||
return auth, ok
|
||||
switch v := ctx.Extra[qqAuthExtraKey].(type) {
|
||||
case qqAuthContext:
|
||||
return v, true
|
||||
case map[string]interface{}:
|
||||
return authFromMap(v)
|
||||
default:
|
||||
return qqAuthContext{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// bindAuthOnFrame 把身份绑到本帧上。调用方需持有 ctx 的写锁。
|
||||
@ -1085,43 +1138,43 @@ func (p *Plugin) activateAuthContext(messageID, userID, groupID int64, isGroup b
|
||||
}
|
||||
p.authGeneration++
|
||||
next := qqAuthContext{
|
||||
active: true, owner: p.isOwner(userID), messageID: messageID, userID: userID,
|
||||
groupID: groupID, isGroup: isGroup, generation: p.authGeneration,
|
||||
Active: true, Owner: p.isOwner(userID), MessageID: messageID, UserID: userID,
|
||||
GroupID: groupID, IsGroup: isGroup, Generation: p.authGeneration,
|
||||
}
|
||||
if messageID != 0 {
|
||||
p.authByMessageID[messageID] = next
|
||||
if len(p.authByMessageID) > 2048 {
|
||||
cutoff := p.authGeneration - 1024
|
||||
for id, auth := range p.authByMessageID {
|
||||
if auth.generation < cutoff {
|
||||
if auth.Generation < cutoff {
|
||||
delete(p.authByMessageID, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !p.auth.active {
|
||||
if !p.auth.Active {
|
||||
p.auth = next
|
||||
return
|
||||
}
|
||||
if p.auth.userID == userID && p.auth.groupID == groupID && p.auth.isGroup == isGroup {
|
||||
p.auth.owner = p.auth.owner && next.owner
|
||||
p.auth.messageID = next.messageID
|
||||
p.auth.generation = next.generation
|
||||
if p.auth.UserID == userID && p.auth.GroupID == groupID && p.auth.IsGroup == isGroup {
|
||||
p.auth.Owner = p.auth.Owner && next.Owner
|
||||
p.auth.MessageID = next.MessageID
|
||||
p.auth.Generation = next.Generation
|
||||
return
|
||||
}
|
||||
// 不同可信来源被内核合并到同一推理时,只在双方都是所有者时保留完整权限。
|
||||
bothOwners := p.auth.owner && next.owner
|
||||
sameGroup := p.auth.isGroup && next.isGroup && p.auth.groupID == groupID
|
||||
p.auth.owner = bothOwners
|
||||
p.auth.messageID = 0
|
||||
p.auth.userID = 0
|
||||
p.auth.isGroup = sameGroup
|
||||
bothOwners := p.auth.Owner && next.Owner
|
||||
sameGroup := p.auth.IsGroup && next.IsGroup && p.auth.GroupID == groupID
|
||||
p.auth.Owner = bothOwners
|
||||
p.auth.MessageID = 0
|
||||
p.auth.UserID = 0
|
||||
p.auth.IsGroup = sameGroup
|
||||
if sameGroup {
|
||||
p.auth.groupID = groupID
|
||||
p.auth.GroupID = groupID
|
||||
} else {
|
||||
p.auth.groupID = 0
|
||||
p.auth.GroupID = 0
|
||||
}
|
||||
p.auth.generation = next.generation
|
||||
p.auth.Generation = next.Generation
|
||||
}
|
||||
|
||||
var qqMessageIDsRe = regexp.MustCompile(`message_id=(-?\d+(?:,-?\d+)*)`)
|
||||
@ -1156,7 +1209,7 @@ func (p *Plugin) onInputAuthContext(ctx *sdk.StageContext) error {
|
||||
|
||||
p.authMu.Lock()
|
||||
// 默认降权:QQ 来源却对不上可信事件时绝不复用上一条消息的身份。
|
||||
next := qqAuthContext{active: source == p.name}
|
||||
next := qqAuthContext{Active: source == p.name}
|
||||
ids := messageIDsFromInput(raw)
|
||||
if source == p.name && len(ids) > 0 {
|
||||
if auth, ok := p.authByMessageID[ids[0]]; ok {
|
||||
@ -1201,9 +1254,9 @@ func (p *Plugin) currentToolAllowed(ctx *sdk.StageContext, name string) (bool, q
|
||||
auth = p.auth
|
||||
}
|
||||
var patterns []string
|
||||
if auth.active && !auth.owner {
|
||||
if auth.isGroup {
|
||||
patterns, _ = p.groupToolAllowlists[auth.groupID]
|
||||
if auth.Active && !auth.Owner {
|
||||
if auth.IsGroup {
|
||||
patterns, _ = p.groupToolAllowlists[auth.GroupID]
|
||||
if patterns == nil {
|
||||
patterns = p.groupToolAllowlists[0]
|
||||
}
|
||||
@ -1212,7 +1265,7 @@ func (p *Plugin) currentToolAllowed(ctx *sdk.StageContext, name string) (bool, q
|
||||
}
|
||||
}
|
||||
p.authMu.RUnlock()
|
||||
if !auth.active || auth.owner {
|
||||
if !auth.Active || auth.Owner {
|
||||
return true, auth
|
||||
}
|
||||
return matchesToolAllowlist(name, patterns), auth
|
||||
@ -1309,7 +1362,7 @@ func (p *Plugin) beforeToolcall(ctx *sdk.StageContext) error {
|
||||
}
|
||||
tc := &ctx.ToolCalls[0]
|
||||
allowed, auth := p.currentToolAllowed(ctx, tc.Name)
|
||||
if !auth.active {
|
||||
if !auth.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1352,7 +1405,7 @@ func (p *Plugin) beforeToolcall(ctx *sdk.StageContext) error {
|
||||
p.setDenial(ctx, msg)
|
||||
return nil
|
||||
}
|
||||
if !auth.owner && isHardPrivateTool(tc.Name) {
|
||||
if !auth.Owner && isHardPrivateTool(tc.Name) {
|
||||
p.authMu.Lock()
|
||||
p.denialLocked = true
|
||||
p.authMu.Unlock()
|
||||
@ -1362,8 +1415,8 @@ func (p *Plugin) beforeToolcall(ctx *sdk.StageContext) error {
|
||||
}
|
||||
if !allowed {
|
||||
scope := "非所有者私聊"
|
||||
if auth.isGroup {
|
||||
scope = fmt.Sprintf("群聊 %d", auth.groupID)
|
||||
if auth.IsGroup {
|
||||
scope = fmt.Sprintf("群聊 %d", auth.GroupID)
|
||||
}
|
||||
msg := fmt.Sprintf("QQ 权限策略拒绝工具 %s:%s 的工具白名单未包含该工具;请不要重试,改为直接说明权限限制", tc.Name, scope)
|
||||
p.setDenial(ctx, msg)
|
||||
|
||||
Reference in New Issue
Block a user