package session import ( "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "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. func ResolveActor(msg adapter.UnifiedMessage, owner *user.OwnerProfile) adapter.Actor { actor := adapter.Actor{ Role: msg.Sender.Role, UserID: firstNonEmpty(msg.Sender.UserID, msg.SenderID), DisplayName: firstNonEmpty(msg.Sender.DisplayName, msg.SenderName), IsGroup: msg.Sender.IsGroup || msg.IsGroup(), GroupID: firstNonEmpty(msg.Sender.GroupID, msg.GroupID), 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 { 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() } // 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 // group context). A non-group owner maps to owner (full access); a non-group // non-owner maps to stranger. func EnvFor(actor adapter.Actor) restrict.ActorEnv { if actor.IsGroup { return restrict.ActorGroupMember } if actor.IsOwner() { return restrict.ActorOwner } 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. func IdentifyContext(msg adapter.UnifiedMessage) restrict.ActorEnv { actor := msg.Sender.Resolve() if msg.IsGroup() { actor.IsGroup = true } return EnvFor(actor) } // Env returns the restrict actor environment for the session. It mirrors // EnvFor: group chats are group_member regardless of membership, owner private // is owner, and anything else is stranger. func (s Session) Env() restrict.ActorEnv { if s.IsGroup { return restrict.ActorGroupMember } if s.Owner { return restrict.ActorOwner } return restrict.ActorStranger } // firstNonEmpty returns a when a is non-empty, otherwise b. func firstNonEmpty(a, b string) string { if a != "" { return a } return b }