用户(第二次、说明白了):「我要求的是不同 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(迁移期放行)。
69 lines
3.4 KiB
Go
69 lines
3.4 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 不同收件箱,而不是复杂的权限隔离……每个 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
|
||
}
|