用户订正:「是应该发到对应 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(伪造)。
81 lines
3.9 KiB
Go
81 lines
3.9 KiB
Go
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
|
||
}
|