86 lines
3.2 KiB
Go
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
|
|
}
|