Files
picoclaw/pkg/session/key.go
T

206 lines
6.1 KiB
Go

package session
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"github.com/sipeed/picoclaw/pkg/routing"
)
const (
sessionKeyV1Prefix = "sk_v1_"
legacyAgentSessionKeyPrefix = "agent:"
)
type ParsedLegacySessionKey struct {
AgentID string
Rest string
}
// BuildOpaqueSessionKey returns a stable opaque session key derived from a
// canonical alias string. The alias remains available through metadata for
// compatibility and migration purposes.
func BuildOpaqueSessionKey(alias string) string {
normalized := strings.TrimSpace(strings.ToLower(alias))
if normalized == "" {
return ""
}
sum := sha256.Sum256([]byte(normalized))
return sessionKeyV1Prefix + hex.EncodeToString(sum[:])
}
// IsOpaqueSessionKey returns true when the key matches the current opaque
// session-key format.
func IsOpaqueSessionKey(key string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(key)), sessionKeyV1Prefix)
}
func IsLegacyAgentSessionKey(key string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(key)), legacyAgentSessionKeyPrefix)
}
func IsExplicitSessionKey(key string) bool {
return IsOpaqueSessionKey(key) || IsLegacyAgentSessionKey(key)
}
func ParseLegacyAgentSessionKey(sessionKey string) *ParsedLegacySessionKey {
raw := strings.TrimSpace(sessionKey)
if raw == "" {
return nil
}
parts := strings.SplitN(raw, ":", 3)
if len(parts) < 3 || parts[0] != "agent" {
return nil
}
agentID := strings.TrimSpace(parts[1])
rest := parts[2]
if agentID == "" || rest == "" {
return nil
}
return &ParsedLegacySessionKey{AgentID: agentID, Rest: rest}
}
// ResolveAgentID returns the routed agent ID associated with a session. It
// prefers structured session scope metadata when available and falls back to
// legacy agent-scoped session keys for compatibility.
func ResolveAgentID(store any, sessionKey string) string {
if scopeReader, ok := store.(interface {
GetSessionScope(sessionKey string) *SessionScope
}); ok {
scope := scopeReader.GetSessionScope(sessionKey)
if scope != nil && strings.TrimSpace(scope.AgentID) != "" {
return routing.NormalizeAgentID(scope.AgentID)
}
}
if parsed := ParseLegacyAgentSessionKey(sessionKey); parsed != nil {
return routing.NormalizeAgentID(parsed.AgentID)
}
return ""
}
func BuildLegacyMainAlias(agentID string) string {
return fmt.Sprintf("agent:%s:main", routing.NormalizeAgentID(agentID))
}
// BuildMainSessionKey returns the canonical opaque main-session key for an
// agent. The corresponding legacy alias remains available via
// BuildLegacyMainAlias for compatibility and migration logic.
func BuildMainSessionKey(agentID string) string {
return BuildOpaqueSessionKey(BuildLegacyMainAlias(agentID))
}
func BuildLegacyDirectAliases(agentID, channel, account, peerID string) []string {
agentID = routing.NormalizeAgentID(agentID)
channel = normalizeLegacyChannel(channel)
account = routing.NormalizeAccountID(account)
peerID = strings.ToLower(strings.TrimSpace(peerID))
if peerID == "" {
return nil
}
return []string{
fmt.Sprintf("agent:%s:direct:%s", agentID, peerID),
fmt.Sprintf("agent:%s:%s:direct:%s", agentID, channel, peerID),
fmt.Sprintf("agent:%s:%s:%s:direct:%s", agentID, channel, account, peerID),
}
}
func BuildLegacyPeerAlias(agentID, channel, peerKind, peerID string) string {
agentID = routing.NormalizeAgentID(agentID)
channel = normalizeLegacyChannel(channel)
peerKind = strings.ToLower(strings.TrimSpace(peerKind))
if peerKind == "" {
peerKind = "unknown"
}
peerID = strings.ToLower(strings.TrimSpace(peerID))
if peerID == "" {
peerID = "unknown"
}
return fmt.Sprintf("agent:%s:%s:%s:%s", agentID, channel, peerKind, peerID)
}
// CanonicalSessionIdentityID collapses an identity using identity_links when
// possible, then returns a normalized lowercase identifier.
func CanonicalSessionIdentityID(channel, rawID string, identityLinks map[string][]string) string {
normalizedID := strings.TrimSpace(rawID)
if normalizedID == "" {
return ""
}
if linked := resolveLinkedPeerID(identityLinks, channel, normalizedID); linked != "" {
normalizedID = linked
}
return strings.ToLower(normalizedID)
}
func normalizeLegacyChannel(channel string) string {
channel = strings.ToLower(strings.TrimSpace(channel))
if channel == "" {
return "unknown"
}
return channel
}
func resolveLinkedPeerID(identityLinks map[string][]string, channel, peerID string) string {
if len(identityLinks) == 0 {
return ""
}
peerID = strings.TrimSpace(peerID)
if peerID == "" {
return ""
}
candidates := make(map[string]bool)
rawCandidate := strings.ToLower(peerID)
if rawCandidate != "" {
candidates[rawCandidate] = true
}
channel = strings.ToLower(strings.TrimSpace(channel))
if channel != "" {
candidates[fmt.Sprintf("%s:%s", channel, rawCandidate)] = true
}
if idx := strings.Index(rawCandidate, ":"); idx > 0 && idx < len(rawCandidate)-1 {
candidates[rawCandidate[idx+1:]] = true
}
for canonical, ids := range identityLinks {
canonicalName := strings.TrimSpace(canonical)
if canonicalName == "" {
continue
}
for _, id := range ids {
normalized := strings.ToLower(strings.TrimSpace(id))
if normalized != "" && candidates[normalized] {
return canonicalName
}
}
}
return ""
}
// CanonicalScopeSignature returns a stable serialized representation of scope.
func CanonicalScopeSignature(scope SessionScope) string {
parts := []string{
fmt.Sprintf("v=%d", scope.Version),
fmt.Sprintf("agent=%s", strings.TrimSpace(strings.ToLower(scope.AgentID))),
fmt.Sprintf("channel=%s", strings.TrimSpace(strings.ToLower(scope.Channel))),
fmt.Sprintf("account=%s", strings.TrimSpace(strings.ToLower(scope.Account))),
}
for _, dimension := range scope.Dimensions {
dimension = strings.TrimSpace(strings.ToLower(dimension))
if dimension == "" {
continue
}
value := strings.TrimSpace(strings.ToLower(scope.Values[dimension]))
parts = append(parts, fmt.Sprintf("%s=%s", dimension, value))
}
return strings.Join(parts, "|")
}
// BuildSessionKey returns the current opaque key for a structured session scope.
func BuildSessionKey(scope SessionScope) string {
return BuildOpaqueSessionKey(CanonicalScopeSignature(scope))
}