fix(channel,user): owner 车道只认白名单,堵住群主/伪造 role 冒充主人(裁决 06.4.2/06.4.3/06.4.4)

- 适配器只描述平台身份:qq_obv11 的 owner/admin 一律上报 RoleMember,不再上报 RoleOwner
- session 层唯一判主点只认白名单(owner.yaml Accounts ∪ 每渠道 adminIds),nil 档案 fail-closed 拒绝
- 新增「每渠道管理员层」:channels.json ChannelAccount.adminIds[] → runtimeconfig.AdminIDs → OwnerProfile.SetPlatformAdmins(整体替换语义,热重载后移除的 id 立即失效)
- 组合根接线:启动即播种、面板保存先灌白名单再重载适配器(cmd/cyrene/wire_channels.go + 4 条端到端单测)
- 06.4.3 节流与参与度状态按会话独立计数(ConversationKey 三处统一,空会话 GC);06.4.4 未知平台主动发言保守默认 1 条/分钟
This commit is contained in:
AskaEth committed 2026-10-04 12:07:40 +08:00
1 parent 7e7f53bb41
commit af8591b78a
30 files changed
+1289 -144

No files matched your search

+32 -19
View File
@@ -6,13 +6,14 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
)
// ResolveActor turns a normalized inbound message into the strongly typed actor
// metadata used by every downstream permission decision. It is the structural
// owner-recognition rule from design/11 §0: the owner is decided by matching
// channel-level identity (platform + account id) against the user.OwnerProfile
// whitelist (or by honoring a channel-level owner tag), and never by reading the
// message text. A message whose body says "I am the owner" is not the owner
// unless it also matches the whitelist.
// ResolveActor 把一条归一化入站消息解析成下游权限决策所用的强类型 actor 元数据。
// 这是 design/11 §0 的结构化主人识别规则(裁决 06.4.2):**owner 车道只认白名单**,
// 判主依据只有结构化来源——主人档案的账号白名单(owner.yaml,internal/user)与每渠道
// 账号配置的 adminIds[](channels.json,design/08 §1.1)。
//
// **适配器上报的角色标记一律不得单独判主**:平台消息里的 role 字段(OneBot 的群主
// owner / 群管理员 admin,或私聊帧里伪造的 role=owner)只能当作非主人侧提示,命中
// owner 也会被降级为 RoleMember。判主永远不看消息正文——正文声称「我是主人」同样无效。
func ResolveActor(msg adapter.UnifiedMessage, owner *user.OwnerProfile) adapter.Actor {
actor := adapter.Actor{
Role: msg.Sender.Role,
@@ -23,19 +24,30 @@ func ResolveActor(msg adapter.UnifiedMessage, owner *user.OwnerProfile) adapter.
GroupName: firstNonEmpty(msg.Sender.GroupName, msg.GroupName),
}
// Structural owner resolution: the whitelist is authoritative, and a
// channel-level owner tag (from authenticated login binding) is also
// structural. Neither depends on msg.Content.
if (owner != nil && owner.IsOwner(msg.Platform, msg.SenderID)) || actor.Role == adapter.RoleOwner {
// 白名单命中即主人;未命中则不管适配器上报了什么角色,都留在非主人车道
// (owner.IsOwner 对 nil 档案安全返回 false)。
if owner.IsOwner(msg.Platform, msg.SenderID) {
actor.Role = adapter.RoleOwner
} else if !actor.Role.Valid() {
// No channel-tagged role and not the owner: default to a member rather
// than leaving the role ambiguous, which is the conservative choice.
actor.Role = adapter.RoleMember
return actor.Resolve()
}
actor.Role = nonOwnerRole(actor.Role)
return actor.Resolve()
}
// nonOwnerRole 把适配器上报的角色收口到非主人车道:owner(群主 / 群管理员 / 伪造
// 字段)一律降级为 RoleMember;member / system / unknown 原样保留;非法值保守回落
// RoleMember,避免留下含糊角色。
func nonOwnerRole(role adapter.Role) adapter.Role {
switch role {
case adapter.RoleOwner:
return adapter.RoleMember
case adapter.RoleMember, adapter.RoleSystem, adapter.RoleUnknown:
return role
default:
return adapter.RoleMember
}
}
// EnvFor maps a resolved actor to the restrict.ActorEnv used to gate tools.
// Group chats always map to group_member (isolated, restricted) even when the
// owner is the speaker (design/02 §3: owner group speech stays in the isolated
@@ -51,15 +63,16 @@ func EnvFor(actor adapter.Actor) restrict.ActorEnv {
return restrict.ActorStranger
}
// IdentifyContext returns the restrict.ActorEnv for a message from the channel
// metadata it carries (msg.Sender), inferring the group flag from the channel
// type when the sender is not already tagged. This is the environment the policy
// engine is bound to before any tool call.
// IdentifyContext 仅凭消息自带的渠道元数据推断 restrict.ActorEnv:群聊一律
// group_member,其余一律 stranger。它拿不到白名单,因此**不能授予 owner 车道**——
// 适配器上报的 role=owner 在此被降级为 member(裁决 06.4.2,owner 只认白名单)。
// 真正的判主走 ResolveActor(msg, owner) + EnvFor。
func IdentifyContext(msg adapter.UnifiedMessage) restrict.ActorEnv {
actor := msg.Sender.Resolve()
if msg.IsGroup() {
actor.IsGroup = true
}
actor.Role = nonOwnerRole(actor.Role)
return EnvFor(actor)
}