fix(addressing)!: 寻址不按工作区筛 + 联系人地址不再从垃圾 from_workspace 拼
用户:「任意 agent 的寻址是任意的,而不是按工作区区分,去落实吧」。 ① 拆掉两处"按工作区收窄"(那是我把**寻址**当成了**权限**): · AgentListContacts 不再走 ListContactsInWorkspace ⇒ 列表 = 我参与过的会话(事实,不是授权); · AgentSuggestAddress 的会话候选不再走 SuggestSessionCandidatesInWorkspace。 两个 InWorkspace 变体(连同钉旧口径的用例)一并删除,避免死代码。 生产实测:pi 的联系人从"只剩同工作区"变成 5 条,横跨 TrueAgent/agentmail/其它工作区。 agentScope 仍调用(校验 session_id 格式 + 未声明时告警),只是它的工作区不再当过滤器。 ② 联系人地址的 path 取自**会话**,不再取第一封邮件的 from_workspace: `COALESCE(NULLIF(s.workspace,''), NULLIF(m.to_workspace,''), '')`。 那条老路把地址拼成 `zcode@zcode.<别名>` —— 正是用户预言的"感染":一个可被复制出去的 错误地址。from_workspace 与"对方在哪"无关,只用 to_*。 ③ **清除感染源(数据)**:658 行 from_workspace = from_name(pi 406 / dsh 137 / zcode 89 / homeagent 17 / opencode 9)已清空。带去重前备份(/root/gotmp/agentmail-pre-fromws-purge-*.db) 与回滚脚本,回滚**在副本库上真跑过**(恢复 658 行)才敢落地。
This commit is contained in:
@ -37,7 +37,6 @@ import (
|
||||
- 列表类接口的反向对照:不带收窄时两条会话都在(证明收窄真的在起作用)。
|
||||
*/
|
||||
|
||||
// sessionIn 在工作区 ws 里造一条会话,并让它与该 Agent 有过一次往来。
|
||||
func sessionIn(t *testing.T, agent, ws, title string) uuid.UUID {
|
||||
t.Helper()
|
||||
id, err := CreateSession(context.Background(), nil, "human", title, ws)
|
||||
@ -48,108 +47,6 @@ func sessionIn(t *testing.T, agent, ws, title string) uuid.UUID {
|
||||
return id
|
||||
}
|
||||
|
||||
func TestAgentMayReadSessionIsSessionScoped(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
const agent = "pi"
|
||||
const wsA = "/home/program/agentmail"
|
||||
const wsB = "/home/program/TrueAgent"
|
||||
|
||||
scope := sessionIn(t, agent, wsA, "我当前在 A 的这条会话里")
|
||||
sameWS := sessionIn(t, agent, wsA, "A 的同工作区、但另一条会话")
|
||||
otherWS := sessionIn(t, agent, wsB, "B 的另一条会话")
|
||||
|
||||
// ① 我就是这条会话 → 放行。少了这条,判据"永远拒绝"也能绿。
|
||||
if ok, reason, err := AgentMayReadSession(ctx, agent, &scope, scope); err != nil || !ok {
|
||||
t.Fatalf("自己当前这条会话必须能读(ok=%v reason=%q err=%v)", ok, reason, err)
|
||||
}
|
||||
|
||||
// ② 同工作区的**另一条**会话 → 拒。会话是私有单位,同工作区不构成通行证
|
||||
// (旧判据在这里是放行的,那等于会话之间可以互相翻收件箱)。
|
||||
ok, reason, err := AgentMayReadSession(ctx, agent, &scope, sameWS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("★ 同工作区但不同会话必须拒 —— 每个 session 是独立单位,收件箱不共用")
|
||||
}
|
||||
if reason != "not-your-session" {
|
||||
t.Fatalf("拒绝原因应是 not-your-session(实际 %q)", reason)
|
||||
}
|
||||
|
||||
// ③ 别的会话(工作区也不同)→ 同一条判据、同一个理由。
|
||||
// ★ 不再用工作区解释:工作区是 cwd/沙箱那条轴的事。zcode 2026-09-15 正是被
|
||||
// cross-workspace 那句误导,推断出"那五位 agent 不存在"。
|
||||
ok, reason, err = AgentMayReadSession(ctx, agent, &scope, otherWS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok || reason != "not-your-session" {
|
||||
t.Fatalf("别的会话必须拒且理由是 not-your-session(ok=%v reason=%q)", ok, reason)
|
||||
}
|
||||
|
||||
// ④ 未声明当前会话 = 迁移期旧语义放行(尚未接线的桥走这条路;服务端另记警告)。
|
||||
if ok, _, err := AgentMayReadSession(ctx, agent, nil, otherWS); err != nil || !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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListContactsInWorkspace(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
const agent = "pi"
|
||||
const wsA = "/home/program/agentmail"
|
||||
const wsB = "/home/program/TrueAgent"
|
||||
|
||||
sessionIn(t, agent, wsA, "A 的线索")
|
||||
sessionIn(t, agent, wsB, "B 的线索")
|
||||
|
||||
// 反向对照:不带工作区收窄时两条都在(证明下一段的收窄真的在起作用)。
|
||||
all, err := ListContactsFor(ctx, agent, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("不带收窄时应当两条都在(实际 %d 条)", len(all))
|
||||
}
|
||||
|
||||
scoped, err := ListContactsInWorkspace(ctx, agent, wsA, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(scoped) != 1 {
|
||||
t.Fatalf("★ 收窄到 A 之后应当只剩 1 条(实际 %d 条)", len(scoped))
|
||||
}
|
||||
if scoped[0].Subject != "A 的线索" {
|
||||
t.Fatalf("留下来的应当是 A 的线索(实际 %q)", scoped[0].Subject)
|
||||
}
|
||||
|
||||
// 另一侧也要验:方向反了(总是返回第一条)同样能骗过上面那两条。
|
||||
scopedB, err := ListContactsInWorkspace(ctx, agent, wsB, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(scopedB) != 1 || scopedB[0].Subject != "B 的线索" {
|
||||
t.Fatalf("收窄到 B 应当只剩 B 的线索(实际 %d 条)", len(scopedB))
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员那条路(scope 为空 = 看全部)必须能跑通。
|
||||
//
|
||||
// ★ 这条是顺手修掉的真 bug:未读计数子查询里一直有 `r.reader_name = $1`,
|
||||
// 而原先的写法是"forUser 为空就不传参" —— $1 于是悬空。Postgres 直接报
|
||||
// `no parameter $1`;SQLite 把 `= $1` 当 `= NULL` 比,次次不成立,未读计数
|
||||
// 静默退化成"全部未归档"。判据只钉"不报错 + 能列出会话"这两条硬事实。
|
||||
func TestListContactsWithEmptyScopeStillWorks(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
@ -164,69 +61,3 @@ func TestListContactsWithEmptyScopeStillWorks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuggestSessionCandidatesInWorkspace(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
const agent = "pi"
|
||||
const wsA = "/home/program/agentmail"
|
||||
const wsB = "/home/program/TrueAgent"
|
||||
|
||||
// 别名是候选列表的可见内容,所以两条会话都要有别名。
|
||||
aliasA, aliasB := "线索-a", "线索-b"
|
||||
mustCreateNamed := func(alias, ws, title string) {
|
||||
t.Helper()
|
||||
id, err := CreateSession(ctx, &alias, "human", title, ws)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedMailInSession(t, id, agent)
|
||||
}
|
||||
mustCreateNamed(aliasA, wsA, "A 的线索")
|
||||
mustCreateNamed(aliasB, wsB, "B 的线索")
|
||||
|
||||
// 同工作区:路径与调用方工作区一致 → 候选照常给出。
|
||||
// 少了这条,"永远返回空"也能骗过下面那条。
|
||||
sameWS, err := SuggestSessionCandidatesInWorkspace(ctx, agent, agent, wsA, wsA)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hasAlias(sameWS, aliasA) {
|
||||
t.Fatalf("本工作区的候选项该列出来(实际 %v)", aliasesOf(sameWS))
|
||||
}
|
||||
|
||||
// 反向对照:**不带**工作区收窄时,去查 B 的路径是能列出 B 的会话别名的
|
||||
// —— 那就是收窄前那个状态(跨工作区可浏览)。
|
||||
raw, err := SuggestSessionCandidates(ctx, agent, agent, wsB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hasAlias(raw, aliasB) {
|
||||
t.Fatalf("不带收窄时 B 的候选本该列出来(实际 %v)—— 对照组不成立,判据就是空的", aliasesOf(raw))
|
||||
}
|
||||
|
||||
// ★ 带上调用方的工作区(人在 A,去查 B 的路径):B 的会话别名不能再出现。
|
||||
cross, err := SuggestSessionCandidatesInWorkspace(ctx, agent, agent, wsB, wsA)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hasAlias(cross, aliasB) {
|
||||
t.Fatalf("★ 别的工作区的会话别名不该被列出来(实际 %v)", aliasesOf(cross))
|
||||
}
|
||||
}
|
||||
|
||||
func hasAlias(list []SessionCandidate, alias string) bool {
|
||||
for _, c := range list {
|
||||
if c.Alias == alias {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func aliasesOf(list []SessionCandidate) []string {
|
||||
out := make([]string, 0, len(list))
|
||||
for _, c := range list {
|
||||
out = append(out, c.Alias)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user