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(迁移期放行)。
This commit is contained in:
2026-09-15 10:01:24 +08:00
parent 6b86836343
commit 81ec77ae1f
2 changed files with 42 additions and 71 deletions

View File

@ -56,68 +56,51 @@ func TestAgentMayReadSessionIsSessionScoped(t *testing.T) {
const wsB = "/home/program/TrueAgent"
scope := sessionIn(t, agent, wsA, "我当前在 A 的这条会话里")
sameWS := sessionIn(t, agent, wsA, "A 的同工作区、但另一条线索")
otherWS := sessionIn(t, agent, wsB, "B 的线索")
alien := sessionIn(t, "someone-else", wsA, "与我无关的线索")
sameWS := sessionIn(t, agent, wsA, "A 的同工作区、但另一条会话")
otherWS := sessionIn(t, agent, wsB, "B 的另一条会话")
// ① 就是我当前这条会话 → 放行。少了这条,判据"永远拒绝"也能绿。
ok, reason, err := AgentMayReadSession(ctx, agent, &scope, scope)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatalf("自己当前这条会话必须能读,却被拒(reason=%q)", reason)
// ① 我就是这条会话 → 放行。少了这条,判据"永远拒绝"也能绿。
if ok, reason, err := AgentMayReadSession(ctx, agent, &scope, scope); err != nil || !ok {
t.Fatalf("自己当前这条会话必须能读(ok=%v reason=%q err=%v)", ok, reason, err)
}
// ② 同工作区的**另一条**会话 → 拒。★ 2026-09-15 用户订正:
// 会话是记忆隔离的单位,"不同 session 不同收件箱" —— 旧判据在这里是放行的
// (它只看工作区),那等于绕过会话隔离。
ok, reason, err = AgentMayReadSession(ctx, agent, &scope, sameWS)
// ② 同工作区的**另一条**会话 → 拒。会话是私有单位,同工作区不构成通行证
// (旧判据在这里是放行的,那等于会话之间可以互相翻收件箱)。
ok, reason, err := AgentMayReadSession(ctx, agent, &scope, sameWS)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("★ 同工作区但不同会话必须拒 —— 会话之间记忆隔离,读别的会话就是绕过它")
t.Fatal("★ 同工作区但不同会话必须拒 —— 每个 session 是独立单位,收件箱不共用")
}
if reason != "not-your-session" {
t.Fatalf("拒绝原因应是 not-your-session(实际 %q)", reason)
}
// ③ 别的会话(工作区也不同)→ 拒,且**不再**用工作区的名义解释
// (工作区是 cwd/沙箱那条轴的事;zcode 2026-09-15 正是被那句 cross-workspace
// 误导,推断出"那些 agent 不存在")。
// ③ 别的会话(工作区也不同)→ 同一条判据、同一个理由。
// ★ 不再用工作区解释:工作区是 cwd/沙箱那条轴的事。zcode 2026-09-15 正是被
// cross-workspace 那句误导,推断出"那五位 agent 不存在"。
ok, reason, err = AgentMayReadSession(ctx, agent, &scope, otherWS)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("别的会话必须拒")
}
if reason != "not-your-session" {
t.Fatalf("拒绝原因应是 not-your-session(实际 %q)—— 别拿工作区当理由", reason)
if ok || reason != "not-your-session" {
t.Fatalf("别的会话必须拒且理由是 not-your-session(ok=%v reason=%q)", ok, reason)
}
// ④ 伪造 session_id:声明 target 就是自己所在会话,但并没参与过 → 拒。
ok, reason, err = AgentMayReadSession(ctx, "pi-not-in-it", &alien, alien)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("没参与过的会话必须拒(防伪造 session_id)")
}
if reason != "not-participant" {
t.Fatalf("拒绝原因应是 not-participant(实际 %q)", reason)
// ④ 未声明当前会话 = 迁移期旧语义放行(尚未接线的桥走这条路;服务端另记警告)。
if ok, _, err := AgentMayReadSession(ctx, agent, nil, otherWS); err != nil || !ok {
t.Fatal("未声明 scope 时应保持旧语义放行(收紧要显式改这条判据)")
}
// ⑤ 未声明当前会话 = 迁移期旧语义放行。**这是妥协不是正确行为**:
// 尚未接线的桥走这条路,所以它必须有判据守着 —— 哪天收紧成"拒",
// 这条会红,逼人去看还有谁没接线。
ok, _, err = AgentMayReadSession(ctx, agent, nil, otherWS)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("未声明 scope 时应保持旧语义放行(迁移期妥协,收紧要显式改这条判据)")
// ⑤ **不做参与性仲裁** —— 这是模型的一部分,钉住它免得以后被"好心"加回来。
// 用户的原话:「不是复杂的权限隔离……每个 session 相当于一个独立的『用户』」。
// 声明自己是哪条会话,就以那条会话的身份行事;信任边界在桥(session_id 由
// worker 闭包注入,模型改不了)。将来若要"session 自带凭据"(每个会话一把钥匙,
// 像每个用户一个账号),那是加**凭据**,不是加这层仲裁 —— 届时这条判据要重写。
outsider := sessionIn(t, "someone-else", wsA, "别人的会话")
if ok, reason, err := AgentMayReadSession(ctx, agent, &outsider, outsider); err != nil || !ok {
t.Fatalf("声明了某条会话即视为以它行事,不该按「该 agent 是否参与过」仲裁(ok=%v reason=%q)", ok, reason)
}
}