// Package user owns the "主人" (owner) profile and account whitelist that the // channel-layer policy engine uses for identity recognition. Per DevDocs/design/11 // §0, owner identification is structural — channel actor metadata matched against // this whitelist — and is never driven by prompt text or an LLM judgment. package user import "sync" // OwnerAccount is a single channel-fingerprint that belongs to the owner. The // policy engine matches channel-layer actor metadata (platform + account id) // against this list. type OwnerAccount struct { Platform string `yaml:"platform"` AccountID string `yaml:"accountId"` } // OwnerProfile is the owner's identity record: a display name, the default // persona bound to the owner, and the per-channel account whitelist. It is the // source data for owner recognition; it is not injected into the persona's // prompt as an authority. // // mu guards Accounts so a live profile can be hot-extended (RegisterAccount) // while the channel bridge's ResolveActor reads it (IsOwner) concurrently. The // mutex is unexported and carries no YAML tag, so it is invisible to encode and // is not part of the persisted profile. type OwnerProfile struct { mu sync.RWMutex `yaml:"-"` ID string `yaml:"id"` DisplayName string `yaml:"displayName"` DefaultPersona string `yaml:"defaultPersona"` Accounts []OwnerAccount `yaml:"accounts"` // IdentityFile is the path to the LLM-visible identity/relationship note // (persona knowledge, NOT an access-control authority). IdentityFile string `yaml:"identityFile,omitempty"` Notes string `yaml:"notes,omitempty"` } // IsOwner reports whether the given (platform, accountID) pair belongs to the // owner according to the whitelist. It is safe for concurrent use with // RegisterAccount (the bridge reads this on every inbound message while a // scan-login registration may extend the whitelist). func (p *OwnerProfile) IsOwner(platform, accountID string) bool { if p == nil { return false } p.mu.RLock() defer p.mu.RUnlock() for _, a := range p.Accounts { if a.Platform == platform && a.AccountID == accountID { return true } } return false } // RegisterAccount appends (platform, accountID) to the whitelist if it is not // already present, leaving the profile otherwise untouched. It is the in-memory // counterpart of RegisterOwnerAccount: the channel bridge holds the very same // *OwnerProfile, so a scan-login confirmation can extend the whitelist without a // restart. Duplicate (platform, accountID) pairs are a no-op, matching IsOwner. func (p *OwnerProfile) RegisterAccount(platform, accountID string) { if p == nil { return } p.mu.Lock() defer p.mu.Unlock() for _, a := range p.Accounts { if a.Platform == platform && a.AccountID == accountID { return } } p.Accounts = append(p.Accounts, OwnerAccount{Platform: platform, AccountID: accountID}) } // AccountIdentifiers returns the distinct channel account ids owned by the // owner, for diagnostics / listing. Safe for concurrent use with RegisterAccount. func (p *OwnerProfile) AccountIdentifiers() []string { if p == nil { return nil } p.mu.RLock() defer p.mu.RUnlock() seen := make(map[string]struct{}, len(p.Accounts)) out := make([]string, 0, len(p.Accounts)) for _, a := range p.Accounts { if _, ok := seen[a.Platform+":"+a.AccountID]; ok { continue } seen[a.Platform+":"+a.AccountID] = struct{}{} out = append(out, a.Platform+":"+a.AccountID) } return out }