Commit Graph

4 Commits

Author SHA1 Message Date
a5fc86bc1a 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 行)才敢落地。
2026-09-15 10:07:18 +08:00
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
6b86836343 fix(read)!: 读信只认会话 —— 删掉「同工作区」那道闸门
用户订正:「是应该发到对应 session 的信,因为 agent 多 session 架构,不同 session 的
记忆是隔离的。你之前那个修法简直荒谬」

我把两条轴混在一起了:**工作区**决定 cwd/沙箱/线索可见面,**会话**决定记忆与收件箱。
原先 AgentMayReadSession 判「参与过 + 同工作区」,两头都错:

  · 太松:同工作区内、我参与过的**别的会话**也放行 —— 会话之间记忆隔离,读别的会话
    就是绕过隔离(用户上午的要求本就是"不同 session 不同收件箱");
  · 太严:我在 A 工作区的上下文里读不到**自己刚发起**、落在 B 工作区那条会话。
    zcode 撞上这条,403 文案还写着 cross-workspace ⇒ 它推断出"那五位 agent 不存在"。

新判据两句:① target 必须就是 scope(我当前那条会话);② 我参与过 target(防伪造
session_id)。scope==nil 保持迁移期放行(五家桥实测都带 session_id)。

403 文案换成「这封信不在你当前所在的那条会话里:每个会话各有各的收件箱(会话之间的
记忆是隔离的)。你当前在会话 <id>。要读它,就在它那条会话里读 —— 每封来信都会把 worker
唤醒到它自己那条会话上」。报"我在哪"安全,目标会话在哪不报。

判据:workspace_scope_test 的该用例改名并重写为五条形态(自己那条会话放行 / 同工作区
另一条会话拒 / 别的工作区会话拒 / 伪造 session_id 拒 / 未声明 scope 迁移期放行)。
生产实测:① 200 ② 403 not-your-session(新文案)③ 403(伪造)。
2026-09-15 09:59:21 +08:00
1b8cd43935 fix(auth): 工作区成为读权限的边界 —— Agent 侧读端点按会话工作区收窄
用户报的:「agentmail 工作区的邮件会话被 trueagent 工作区的 agent 看到了,
还需要我亲自去解释。」

## 根因不是漏了一个 WHERE,是隔离单位选错了

Agent 注册时 `workspaces` 是空的(B-1.2:cwd 由每封邮件的 `to_workspace` 决定),
所以**一个 Agent 同时服务所有工作区**。而可见性判据一直是
`AgentCanAccessSession(agentName, sid)` = "这个 Agent 名出现在这条会话的 from/to/cc 里"
—— 于是同一个 agent `pi`,在 TrueAgent 里干活的 worker 眼里,对 agentmail 的会话
也成立。

现场证据:`mail_reads` 里 08:11–09:19 有 8 次「同一瞬间读了多个不同工作区的会话」
(08:23:59 一次跨 agentmail / TrueAgent / webui4frpc 三条会话),最后一次是 09:19:11
—— 正好停在 `read_inbox` 按会话收窄那个提交(552fbc7,09:19:25)之前。
更要紧的是 `mail_reads` 只记 `reader_name`、**没有「读的人当时在哪个工作区」这一列**,
所以这类越界读在数据上与正常读**无法区分** —— 这也是为什么只能由用户自己去解释。

## 改法:补一维,而不是逐个端点打补丁

- 新增 `repo.AgentMayReadSession(agentName, scope, target)`:① 参与过(原有判据)
  ② 两条会话的 `workspace` 相同(新增)。`scope` = 调用方当前所在的那条会话。
- 服务端只认一条**会话 id**(`?session_id=`),由它反查 workspace ——
  **不接受调用方直接声明工作区**,否则等于让它自己给自己发通行证。
- 应用到四个读端点:`read_mail` / `read_thread` / `session_participants` /
  `list_contacts`,以及 `contacts/suggest` 的**会话候选**(name/path 两段不收窄:
  跨工作区**发信**是设计允许的,被挡的只是"浏览同行的线索")。
- 未声明 `session_id` 时保留旧语义(放行)并**记警告日志**:迁移要能分步走,
  但"还有谁没接线"必须可观测(另四家桥仍走这条路)。
- pi 桥:五个读工具全部带上自己那条邮件会话 id(由 worker 闭包注入,模型改不了)。

## 顺手修掉一个真 bug

联系人查询的未读计数子查询里一直有 `r.reader_name = $1`,而原写法是
"forUser 为空就不传参" ⇒ $1 悬空:Postgres 直接报 `no parameter $1`,
SQLite 把 `= $1` 当 `= NULL` 比、次次不成立(未读计数静默退化成"全部未归档")。
管理员 `?all=true` 走的正是这条路。现在 $1 恒传。

## 判据(两侧都验 + 变异)

- repo:同工作区放行 / 跨工作区拒且 reason 分得清 / 没参与过拒 /
  未声明 scope 的旧语义;列表类有反向对照(不带收窄两条都在);
  建议补全同工作区照常给候选、跨工作区查路径不给、不带收窄会给(对照组)。
- ★ 这条判据我第一版**写错了对照组**:拿 path=wsA 去比 —— 而 path 本来就收窄,
  于是"不带收窄"也只剩一条,判据等于空的。改成拿 path=wsB 比才有区分力。
- 变异 3 处(拿掉工作区判据 / ListContactsInWorkspace 不收窄 /
  SuggestSessionCandidatesInWorkspace 不收窄)⇒ 各自恰好红在对应那条断言。
- pi 桥 14 条:6 个读工具 × 带上/不带 scope 两侧 + worker 闭包 + 自检;
  变异 read_mail 去掉收窄 ⇒ 恰好那一条红。

(工作区是多会话共用的,本次只 add 了 server/ 与 plugins/pi-mail-bridge/ 的 7 个文件。)
2026-09-14 23:03:25 +08:00