fix(session): 群聊/陌生人记忆隔离——MemWritable 贯穿 loop(AGENTS.md 红线)
群聊/陌生人查记忆只读不写、不泄露主人敏感;owner 默认行为完全不变。 - loop/sessionenv.go:SessionEnv 类型 + WithSessionEnv/sessionEnvFrom/OwnerEnv——每次 Run 携带会话 隔离上下文,经 context.Context 传递(不改 Run 签名、不存 Loop/Options 字段,避免 16 并发渠道 消息共享 Loop 的数据竞争;Chatter 接口与测试桩签名不变)。 - loop/memory.go 门控:injectMemory 读——MemWritable=true(owner)走原 Retriever.Inject 全量不变; false(群聊/陌生人)改 Retrieve 后只留 ScopeProject(人人可读)、剔除 ScopeUser(owner习惯私有)与 ScopePersona(角色日常=owner敏感),块首加「群聊隔离」标记,无项目记忆不注入;extractMemory 写 false 直接 return 跳过(不调用 extractor 不落库)。 - 渠道入站(Channelbridge)传真实会话 MemWritable/Env;Web/REST/WS/voice chat 绑 OwnerEnv(JWT=owner 行为不变);backthink/automations/orchestration 默认 owner 全量。 - 测试:memory_gating_test 6 用例(owner/群聊只读不写/陌生人受限/无项目记忆不注入/默认owner全量)全绿; loop/session/gateway 既有测试无回归。go build/vet/test 47 包全绿。 遗留:ScopeProject 过滤在 loop 内后处理实现(未改 memory 包),若后续允许可在 Retriever 加 scope 过滤选项消除与 Inject 模板的重复(2处)。
This commit is contained in:
1 parent
dbcf3df8a3
commit
adad4e1f62
7 files changed
+297
-6
No files matched your search
@@ -313,7 +313,11 @@ func (b *Bridge) process(ctx context.Context, msg adapter.UnifiedMessage) {
|
||||
"session", sess.SessionID, "actor_role", sess.ActorRole,
|
||||
"env", sess.Env(), "mem_writable", sess.MemWritable)
|
||||
|
||||
res, err := b.run.Run(ctx, sess.SessionID, []model.Message{
|
||||
// Thread the resolved isolation boundary into the loop's memory gates: the
|
||||
// owner's private conversation reads+writes memory, group chats and strangers
|
||||
// are read-only isolation (design/02 §3, design/11 §2).
|
||||
runEnv := loop.SessionEnv{ActorEnv: sess.Env(), MemWritable: sess.MemWritable}
|
||||
res, err := b.run.Run(loop.WithSessionEnv(ctx, runEnv), sess.SessionID, []model.Message{
|
||||
model.TextMessage(model.RoleUser, text),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
|
||||
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
@@ -127,7 +128,9 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
if req.SessionID == "" {
|
||||
req.SessionID = "web"
|
||||
}
|
||||
res, err := s.runner.Run(r.Context(), req.SessionID, userChatMessages(req.Text, req.Image))
|
||||
// The Web main session is the owner's private conversation (JWT = owner), so
|
||||
// it binds the full-access env: memory read+write, unchanged behavior.
|
||||
res, err := s.runner.Run(loop.WithSessionEnv(r.Context(), loop.OwnerEnv()), req.SessionID, userChatMessages(req.Text, req.Image))
|
||||
if err != nil {
|
||||
s.log.Error("chat run", "err", err, "session", req.SessionID)
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "chat_failed", "chat failed"))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
|
||||
)
|
||||
@@ -96,7 +97,9 @@ func (h *hub) runVoiceTurn(c *wsConn, req voiceInputRequest) {
|
||||
h.voiceError(c, "chat service is not configured")
|
||||
return
|
||||
}
|
||||
res, err := h.runner.Run(ctx, req.SessionID, []model.Message{model.TextMessage(model.RoleUser, transcript)})
|
||||
// The Web main session is the owner's private conversation, so it binds the
|
||||
// full-access env: memory read+write, unchanged behavior.
|
||||
res, err := h.runner.Run(loop.WithSessionEnv(ctx, loop.OwnerEnv()), req.SessionID, []model.Message{model.TextMessage(model.RoleUser, transcript)})
|
||||
if err != nil {
|
||||
h.log.Warn("voice chat run", "err", err, "session", req.SessionID)
|
||||
h.voiceError(c, "voice chat failed")
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
@@ -223,7 +224,9 @@ func (h *hub) streamChat(ctx context.Context, payload any) {
|
||||
if req.SessionID == "" {
|
||||
req.SessionID = "web"
|
||||
}
|
||||
res, err := h.runner.Run(ctx, req.SessionID, userChatMessages(req.Text, req.Image))
|
||||
// The Web main session is the owner's private conversation (JWT = owner), so
|
||||
// it binds the full-access env: memory read+write, unchanged behavior.
|
||||
res, err := h.runner.Run(loop.WithSessionEnv(ctx, loop.OwnerEnv()), req.SessionID, userChatMessages(req.Text, req.Image))
|
||||
if err != nil {
|
||||
h.log.Error("ws chat run", "err", err, "session", req.SessionID)
|
||||
h.broadcast(wsEvent{Type: "stream_end", TurnID: turnID, Seq: int(h.streamSeq.Add(1)), Data: map[string]any{"stopReason": "error", "error": err.Error()}})
|
||||
|
||||
Reference in new issue
Block a user