Files
MailUI4Agents/server/internal/repo/session_workspace.go
JianFeeeee 81ec77ae1f fix(read)!: 会话即主体 —— 读路径不再做任何"谁有资格"的仲裁
用户(第二次、说明白了):「我要求的是不同 session 不同收件箱,而不是复杂的权限隔离……
每个 session 是相对独立的单位,他们不应该公用一个相对私有化的设施,相当于每个 session
概念上是一个独立的『用户』」

所以 AgentMayReadSession 只剩一句话:**target 必须就是 scope**(我当前所在的那条会话)。
删掉两样东西:
  · 工作区比较 —— 那是 cwd/沙箱那条轴的事,混进读信就制造了 zcode 那次误判
    (cross-workspace 的 403 让它推断"那五位 agent 不存在");
  · **参与性仲裁** —— 那是我加的"相对私有化"设施:把 agent 当成一个跨所有会话的人,
    于是它既太松(同工作区内能互翻收件箱)又太严(发起者读不到自己发起的会话)。
    会话是独立单位,不需要一个更高层身份来"授权"它读自己的收件箱。

信任边界在桥:session_id 由 worker 闭包注入(模型改不了),等同"邮件客户端替它持有的
每个账号行事"。未声明 scope 的旧语义保持放行(记警告)。

判据重写为:① 我就是这条会话 → 放行;② 同工作区另一条会话 → 拒;③ 别的工作区会话 → 拒
(同一个理由 not-your-session);④ 未声明 → 迁移期放行;⑤ **显式钉住"不做参与性仲裁"**
(声明自己是哪条会话即以其行事,即便该 agent 名义上没参与过)—— 这条是模型的一部分,
免得以后被"好心"加回来;将来若要做"每个 session 自带凭据",那是加凭据而不是加回这层仲裁。

生产实测:① 200;② 403「这封信不在你当前所在的那条会话里……」;③ 200(迁移期放行)。
2026-09-15 10:01:24 +08:00

69 lines
3.4 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 不同收件箱,而不是复杂的权限隔离……每个 session 是相对
// 独立的单位,他们不应该公用一个相对私有化的设施,相当于每个 session 概念上是一个
// 独立的『用户』」
//
// 那么这里就不该有任何"谁有资格读谁"的仲裁 —— **会话本身就是那个私有的单位**:
//
// target 必须就是 `scope`(我当前所在的那条会话)。
//
// 别的东西一概不看:不看工作区(那是 cwd/沙箱那条轴的事),也不看"这个 agent 参没参与过"
// (那是我之前加的"相对私有化"设施 —— 它把 agent 当成一个跨越所有会话的人,
// 既制造了越界读(同工作区里能读别的会话),又挡住了正当读者(发起者读不到自己发起的
// 会话,zcode 因此推断出"那五位 agent 不存在")。会话是独立单位,它不需要被一个
// 更高层的身份来"授权"。
//
// 信任边界在**桥**:`session_id` 由 worker 闭包注入(模型改不了),桥是我们的可信组件。
// 这与「一个邮件客户端替它持有的每个账号收发信」是同一种信任:声明自己是哪条会话,
// 就以那条会话的身份行事。首次接线前的旧语义(未声明 scope)保持放行并记警告日志。
func AgentMayReadSession(ctx context.Context, agentName string, scope *uuid.UUID, target uuid.UUID) (bool, string, error) {
_ = ctx
_ = agentName
if scope == nil {
return true, "", nil // 迁移期:未声明当前会话者按旧语义放行(服务端另记警告)
}
if *scope != target {
return false, "not-your-session", nil
}
return true, "", nil
}