Files
MailUI4Agents/server/internal/repo/session_workspace.go
JianFeeeee 6b86836343 fix(read)!: 读信只认会话 —— 删掉「同工作区」那道闸门
用户订正:「是应该发到对应 session 的信,因为 agent 多 session 架构,不同 session 的
记忆是隔离的。你之前那个修法简直荒谬」

我把两条轴混在一起了:**工作区**决定 cwd/沙箱/线索可见面,**会话**决定记忆与收件箱。
原先 AgentMayReadSession 判「参与过 + 同工作区」,两头都错:

  · 太松:同工作区内、我参与过的**别的会话**也放行 —— 会话之间记忆隔离,读别的会话
    就是绕过隔离(用户上午的要求本就是"不同 session 不同收件箱");
  · 太严:我在 A 工作区的上下文里读不到**自己刚发起**、落在 B 工作区那条会话。
    zcode 撞上这条,403 文案还写着 cross-workspace ⇒ 它推断出"那五位 agent 不存在"。

新判据两句:① target 必须就是 scope(我当前那条会话);② 我参与过 target(防伪造
session_id)。scope==nil 保持迁移期放行(五家桥实测都带 session_id)。

403 文案换成「这封信不在你当前所在的那条会话里:每个会话各有各的收件箱(会话之间的
记忆是隔离的)。你当前在会话 <id>。要读它,就在它那条会话里读 —— 每封来信都会把 worker
唤醒到它自己那条会话上」。报"我在哪"安全,目标会话在哪不报。

判据:workspace_scope_test 的该用例改名并重写为五条形态(自己那条会话放行 / 同工作区
另一条会话拒 / 别的工作区会话拒 / 伪造 session_id 拒 / 未声明 scope 迁移期放行)。
生产实测:① 200 ② 403 not-your-session(新文案)③ 403(伪造)。
2026-09-15 09:59:21 +08:00

81 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}