diff --git a/server/internal/repo/session_workspace.go b/server/internal/repo/session_workspace.go index 2144e10..507e73f 100644 --- a/server/internal/repo/session_workspace.go +++ b/server/internal/repo/session_workspace.go @@ -36,45 +36,33 @@ func SessionWorkspaceOf(ctx context.Context, id uuid.UUID) string { // AgentMayReadSession 判「以 `scope` 为当前会话的 Agent 能不能读 `target`」。 // -// ★ 2026-09-15 用户订正(原话:「是应该发到对应 session 的信,因为 agent 多 session -// 架构,不同 session 的记忆是隔离的。你之前那个修法简直荒谬」): +// ★ 2026-09-15 用户把模型说明白了(两次,第二次我看懂了): // -// **读信这条路只认会话,不认工作区。** +// 「我要求的是不同 session 不同收件箱,而不是复杂的权限隔离……每个 session 是相对 +// 独立的单位,他们不应该公用一个相对私有化的设施,相当于每个 session 概念上是一个 +// 独立的『用户』」 // -// ① target 必须就是 `scope`(我当前所在的那条会话)—— 「不同 session 不同收件箱」 -// ② 我确实参与过 target(防伪造 session_id:声明别人的会话也不行) +// 那么这里就不该有任何"谁有资格读谁"的仲裁 —— **会话本身就是那个私有的单位**: // -// # 为什么删掉原先的「工作区相同」那道闸门 +// target 必须就是 `scope`(我当前所在的那条会话)。 // -// 我把两条轴混在了一起:**工作区决定 cwd / 沙箱 / 谁能看到哪些线索**, -// 而**会话决定记忆与收件箱** —— 一个 agent 同时服务多个工作区,正是"多会话" -// 架构存在的理由。于是原先那条判据两头都错: +// 别的东西一概不看:不看工作区(那是 cwd/沙箱那条轴的事),也不看"这个 agent 参没参与过" +// (那是我之前加的"相对私有化"设施 —— 它把 agent 当成一个跨越所有会话的人, +// 既制造了越界读(同工作区里能读别的会话),又挡住了正当读者(发起者读不到自己发起的 +// 会话,zcode 因此推断出"那五位 agent 不存在")。会话是独立单位,它不需要被一个 +// 更高层的身份来"授权"。 // -// · 太松:同一个工作区里,我参与过的**别的会话**也放行了 —— 而会话之间记忆隔离, -// 读别的会话等于绕过隔离(用户的原始要求就是"不同 session 不同收件箱"); -// · 太严:我在 A 工作区的上下文里,读不到**自己刚发起**的、B 工作区那条会话 —— -// 发起者读不到自己的会话(zcode 2026-09-15 撞上这条,403 之后它推断出 -// "那五位 agent 不存在",把错误结论写进了给人的信里)。 -// -// 会话边界一处定死之后,两侧都对了:worker 在哪条会话里,就只看得见那条的收件箱; -// 要读另一条,就得在那条会话里(每封来信都会把 worker 唤醒到它自己那条会话上)。 +// 信任边界在**桥**:`session_id` 由 worker 闭包注入(模型改不了),桥是我们的可信组件。 +// 这与「一个邮件客户端替它持有的每个账号收发信」是同一种信任:声明自己是哪条会话, +// 就以那条会话的身份行事。首次接线前的旧语义(未声明 scope)保持放行并记警告日志。 func AgentMayReadSession(ctx context.Context, agentName string, scope *uuid.UUID, target uuid.UUID) (bool, string, error) { - // 未声明当前会话:无法判断"你人在哪条会话里",按迁移期旧语义放行(服务端另记警告)。 - // 五家桥实测都会带 session_id(agent_discovery 的 agentScope)。 + _ = ctx + _ = agentName if scope == nil { - return true, "", 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 } diff --git a/server/internal/repo/workspace_scope_test.go b/server/internal/repo/workspace_scope_test.go index f4451ad..891abbd 100644 --- a/server/internal/repo/workspace_scope_test.go +++ b/server/internal/repo/workspace_scope_test.go @@ -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) } }