package repo import ( "context" "github.com/agentmail/gateway/internal/db" "github.com/google/uuid" ) // SessionWorkspaceOf 返回**这条会话**的工作目录(`sessions.workspace`)。 // // # 为什么需要它 // // 邮件的 `to_workspace` 是插件唯一能知道「这个任务该在哪个目录干活」的入口, // 但它取的是**地址里的 path 位**。而 Agent 之间的回信、以及人在对话页点回复时, // 地址里通常没有 path 位 —— 平台自己下发的 `reply_address` 就是这个形状。 // // 空着传下去的后果是可观测的:插件只能自己拼一个临时目录,于是**每封邮件落在 // 一个不同的空目录里**;DSH / opencode 按 cwd 给会话分组,界面上就成了「每处理 // 一封邮件就多出一条未分组会话」,而模型在空目录里什么项目文件也看不到。 // // 会话的 workspace 才是权威来源(见 `models.SessionWorkspace` 的注释): // 回信本来就是回给**那条会话**的,而那条会话知道自己属于哪个项目。 // // 读不到时返回空串而不是报错:投递路径不能因为一次查询失败就丢掉工作目录信息, // 但也不能凭空编一个 —— 空串的语义就是「不知道」,由调用方决定怎么退化。 func SessionWorkspaceOf(ctx context.Context, id uuid.UUID) string { var ws string err := db.DB.QueryRowContext(ctx, `SELECT COALESCE(workspace, '') FROM sessions WHERE session_id = $1`, id).Scan(&ws) if err != nil { return "" } return ws } // AgentMayReadSession 判「以 `scope` 为当前会话的 Agent 能不能读 `target`」。 // // ★ 2026-09-15 用户订正(原话:「是应该发到对应 session 的信,因为 agent 多 session // 架构,不同 session 的记忆是隔离的。你之前那个修法简直荒谬」): // // **读信这条路只认会话,不认工作区。** // // ① target 必须就是 `scope`(我当前所在的那条会话)—— 「不同 session 不同收件箱」 // ② 我确实参与过 target(防伪造 session_id:声明别人的会话也不行) // // # 为什么删掉原先的「工作区相同」那道闸门 // // 我把两条轴混在了一起:**工作区决定 cwd / 沙箱 / 谁能看到哪些线索**, // 而**会话决定记忆与收件箱** —— 一个 agent 同时服务多个工作区,正是"多会话" // 架构存在的理由。于是原先那条判据两头都错: // // · 太松:同一个工作区里,我参与过的**别的会话**也放行了 —— 而会话之间记忆隔离, // 读别的会话等于绕过隔离(用户的原始要求就是"不同 session 不同收件箱"); // · 太严:我在 A 工作区的上下文里,读不到**自己刚发起**的、B 工作区那条会话 —— // 发起者读不到自己的会话(zcode 2026-09-15 撞上这条,403 之后它推断出 // "那五位 agent 不存在",把错误结论写进了给人的信里)。 // // 会话边界一处定死之后,两侧都对了:worker 在哪条会话里,就只看得见那条的收件箱; // 要读另一条,就得在那条会话里(每封来信都会把 worker 唤醒到它自己那条会话上)。 func AgentMayReadSession(ctx context.Context, agentName string, scope *uuid.UUID, target uuid.UUID) (bool, string, error) { // 未声明当前会话:无法判断"你人在哪条会话里",按迁移期旧语义放行(服务端另记警告)。 // 五家桥实测都会带 session_id(agent_discovery 的 agentScope)。 if scope == nil { return true, "", nil } // ① 只读自己当前那条会话 —— 不同 session 就是不同收件箱 if *scope != target { return false, "not-your-session", nil } // ② 防伪造 session_id:声明一条自己没参与的会话也不行 participates, err := AgentCanAccessSession(ctx, agentName, target) if err != nil { return false, "", err } if !participates { return false, "not-participant", nil } return true, "", nil }