Files
Cyrene_v2/internal/channel/session/actor.go
T

86 lines
3.2 KiB
Go

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
}