package loop import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict" ) // SessionEnv is the per-conversation context that gates memory injection and // extraction for one Run call. It travels on the call's context rather than on // the shared Loop because Loop is reused across concurrent sessions (every // channel/group/stranger message resolves its own isolation boundary); storing it // on the Loop struct would race. type SessionEnv struct { // ActorEnv is the resolved actor environment: owner, group_member, stranger. // It is the strongly-typed identity lane decided structurally by the channel // layer (design/11 §0), never inferred from prompt text. ActorEnv restrict.ActorEnv // MemWritable reports whether this conversation may write shared memory. It is // true only for the owner's private conversation; group chats and strangers // are read-only (design/02 §3, design/11 §2). MemWritable bool // PersonaID is the role this conversation speaks as (design/22 §2/§4.2), resolved // per turn by the caller (normally the gateway's session-binding resolver). Empty // means "inherit the loop's configured role" (Options.PersonaID, the global default // role), which keeps every pre-multi-character caller — channelbridge, backthink, // internal autonomous passes — behaving exactly as before. It scopes the system // prompt, the memory namespace, the emotion machine and the thinking budget; it is // NOT an identity or permission input: ActorEnv/MemWritable alone decide access // (project red line). PersonaID string } // Restricted reports whether this is a group-chat or stranger conversation, i.e. // the read-only memory isolation context. Because MemWritable is true only for // the owner's private conversation, !MemWritable is exactly the restricted set. func (e SessionEnv) Restricted() bool { return !e.MemWritable } // OwnerEnv returns the full-access owner session environment (private // conversation, memory read+write). It is the Web main-session and internal // automated-task default, matching the pre-existing owner-full behavior. It binds // no explicit role (PersonaID is empty), so the loop falls back to its configured // default role — the caller that knows the session's role sets PersonaID on the // returned value (design/22 §4.2). func OwnerEnv() SessionEnv { return SessionEnv{ActorEnv: restrict.ActorOwner, MemWritable: true} } // defaultSessionEnv is the owner-full environment used when a caller attaches no // explicit env: full memory read and write, the pre-existing behavior. func defaultSessionEnv() SessionEnv { return OwnerEnv() } type sessionEnvKey struct{} // WithSessionEnv attaches env to ctx so Run's memory gates honor the // group/stranger read-only isolation per call. It is concurrency-safe: the env // travels with the call's context and is never stored on the shared Loop. func WithSessionEnv(ctx context.Context, env SessionEnv) context.Context { return context.WithValue(ctx, sessionEnvKey{}, env) } // sessionEnvFrom returns the env attached to ctx, or the owner-full default when // none was attached (backward compatible). func sessionEnvFrom(ctx context.Context) SessionEnv { if ctx == nil { return defaultSessionEnv() } if env, ok := ctx.Value(sessionEnvKey{}).(SessionEnv); ok { return env } return defaultSessionEnv() }