diff --git a/docs/design/architecture-v3.0.md b/docs/design/architecture-v3.0.md index 3e63db1..0a40939 100644 --- a/docs/design/architecture-v3.0.md +++ b/docs/design/architecture-v3.0.md @@ -27,7 +27,7 @@ | v2.7.2 | 2026-05-26 | 防重复调用 & 防无限续杯 + counter v2.1 | | v3.0 | 2026-05-28 | **本版**:PRD 3.0 对齐 + 源码回溯 + 完整现状记录 | | v3.0.1 | 2026-05-28 | 补充6个讨论话题(§6.5/§10.4~10.6)+ 新增§21 T阶段规划(T1~T6) | -| v3.0.2 | 2026-05-29 | §12.3 Mail API 防御 + Prompt 约束设计;§6.5 补充 spawn 改造方向 | +| v3.0.2 | 2026-05-29 | §12.3 Mail API 防御 + Prompt 约束设计(纳入司马懿评审意见);§6.5 补充 spawn 改造方向 | ### 1.2 v3.0 定位 @@ -969,6 +969,7 @@ route(task_info, action_type): ### 12.3 Mail API 防御 + Prompt 约束 📋 > 来源:#02 Main Session + Delegation 设计过程中发现的 Mail 自环问题 +> 司马懿评审:mail-1780058519676 回复(M1/M2/S1-S3/A9),已纳入 #### 问题 @@ -976,22 +977,46 @@ Agent 回复邮件时把 `to` 写成自己,导致自环(mail-1780056763652 1. **API 层无防御**:`send_mail` 不校验 `to` 的合法性,不自动推断回复收件人 2. **Prompt 层无约束**:回复模板中 `to` 字段要求 Agent 手动填写,Agent 容易写错 +#### 设计约束 + +1. **Mail 是 Agent 间 1 对 1 通信**。`from` 和 `to` 都必须是有效 Agent,`from="user"` 不允许(用户收不到邮件) +2. **严格 1 对 1**。禁止第三方插入会话。多 Agent 协作走 Task(广播/mention) +3. **有效 Agent 列表**统一维护一份,API 校验和 Prompt 模板共用同一数据源 + #### API 层防御(`mail_routes.py` `send_mail`) -| # | 场景 | 校验逻辑 | 错误处理 | -|---|------|---------|----------| -| A1 | `from` 缺失/为空 | 必填校验 | 400:"`from` 必填" | -| A2 | `to` 缺失/为空(非回复) | 必填校验 | 400:"`to` 必填" | -| A3 | `from == to`(自环) | 防自环 | 400:"不能给自己发邮件" | -| A4 | `to` 对应的 Agent 不存在 | 从注册 Agent 列表校验 | 400:"`{to}` 不是有效的 Agent" | -| A5 | `in_reply_to` 指向不存在的邮件 | 原邮件存在性校验 | 400:"回复的邮件 `{id}` 不存在" | -| A6 | `in_reply_to` 存在,`to` 与原邮件 `from` 不一致 | **自动纠正** | 用原邮件 `assigned_by` 替换 `to`,响应中提示 | -| A7 | `in_reply_to` 存在,`to` 未传 | **自动填充** | 从原邮件取 `assigned_by` 作为 `to` | -| A8 | `in_reply_to` 存在,`from` 不是原邮件 `to` | 允许(第三方参与会话) | 正常处理 | +**校验执行顺序**(按此顺序逐条校验,任一失败立即返回 400): -**A4 校验方式**:从 ticker 的 `self.agents` 或 Agent 注册表获取有效 Agent ID 列表。硬编码 6 个 Agent(zhangfei-dev / guanyu-dev / zhaoyun-data / jiangwei-infra / pangtong-fujunshi / simayi-challenger)作为初始方案,后续改为从配置读取。 +| 顺序 | # | 场景 | 校验逻辑 | 错误处理 | +|------|---|------|---------|----------| +| 1 | A1 | `from` 缺失/为空 | 必填校验 | 400:"`from` 必填" | +| 2 | A9 | `from` 不是有效 Agent | 从注册 Agent 列表校验 | 400:"`from` 不是有效的 Agent" | +| 3 | A5 | `in_reply_to` 指向不存在的邮件 | 原邮件存在性校验 | 400:"回复的邮件不存在" | +| 4 | A6/A7 | `in_reply_to` 存在时自动纠正 `to` | 自动从原邮件取 `assigned_by`(原发件者)作为 `to` | 静默纠正,响应中加 `auto_corrected` 提示 | +| 5 | A2 | `to` 缺失/为空(非回复) | 必填校验 | 400:"`to` 必填" | +| 6 | A3 | `from == to`(自环) | 防自环 | 400:"不能给自己发邮件" | +| 7 | A4 | `to` 不是有效 Agent | 从注册 Agent 列表校验 | 400:"`to` 不是有效的 Agent" | +| 8 | A8 | `in_reply_to` 存在,`from` 不是原邮件的 `assignee` 或 `assigned_by` | 严格 1 对 1:只有原邮件的双方能回复 | 400:"只有邮件的发送者或接收者可以回复" | +| 9 | A10 | `text` 和 `description` 都为空 | 正文非空校验 | 400:"邮件正文不能为空" | -**自动纠正逻辑**:A6 + A7 合并处理——有 `in_reply_to` 时,`to` 不传或传错都自动从原邮件取 `assigned_by`(原始发件者)。 +**关键设计说明**: + +- **A6/A7 自动纠正**:有 `in_reply_to` 时,无论 Agent 传什么 `to`,都自动从原邮件取 `assigned_by`(黑板字段,即原发件者)作为回复的 `to`。回复方向固定为 reply → original sender,不支持自定义收件人 +- **A8 严格 1 对 1**:只有原邮件的 `assigned_by`(发件者)和 `assignee`(收件者)可以回复。其他人想参与需开新邮件,不能用 `in_reply_to` 插入别人的对话 +- **A9 `from` 校验**:`from` 必须是有效 Agent。现有代码 `body.get("from", "user")` 的默认值 `"user"` 需移除,改为必填 +- **A4/A9 有效 Agent 列表**:同一数据源(从 ticker `self.agents` 或配置读取)。Prompt 模板也用此列表的 `{valid_agents}` 变量替换 + +**A6 自动纠正响应格式**: + +```json +{ + "ok": true, + "mail_id": "mail-xxx", + "auto_corrected": {"field": "to", "original": "simayi-challenger", "corrected": "pangtong-fujunshi"} +} +``` + +让调用者知道 `to` 被纠正了,Agent 下次可以避免同样的错误。 #### Prompt 层约束(`spawner.py` 模板) @@ -999,7 +1024,7 @@ Agent 回复邮件时把 `to` 写成自己,导致自环(mail-1780056763652 1. 回复模板中 curl 命令的 JSON 双花括号转义,Agent 容易写错 2. 没有告诉 Agent "to 会自动推断",Agent 手动填写容易出错 3. 没有给 Agent "发新邮件" 的模板,Agent 自己猜格式 -4. 没有列出有效 Agent ID,Agent 可能给不存在的 agent 发邮件 +4. Agent ID 列表硬编码在模板字符串里,加 Agent 要改代码 **改动**: @@ -1024,7 +1049,7 @@ curl -s -X POST http://localhost:8083/api/mail \\ -H 'Content-Type: application/json' \\ -d '{{"from": "{agent_id}", "to": "对方agent-id", "title": "标题", "text": "正文", "type": "inform"}}' -⚠️ to 必须是有效的 agent id(zhangfei-dev / guanyu-dev / zhaoyun-data / jiangwei-infra / pangtong-fujunshi / simayi-challenger) +⚠️ to 必须是有效的 agent id: {valid_agents} ⚠️ 纯通知用 type=inform,需要对方回复不填 type(默认 request) ⚠️ 不能给自己发邮件 ⚠️ 不要执行任何状态转换命令(标 working/done/review/failed 等),系统会自动处理。 @@ -1036,27 +1061,28 @@ MAIL_INFORM_TEMPLATE = """你收到一封飞鸽传书(纯通知)。 主题: {title} 内容: {text} -已阅即可。如需回复,同上用 in_reply_to 回复发件者。 +已阅即可。如需回复,用 in_reply_to 回复发件者(不需要填 to)。 ⚠️ 不要执行任何状态转换命令。 """ ``` **关键改动**: 1. 回复模板去掉 `to` 字段,告诉 Agent "系统自动回复给发件者" -2. 新增"给其他人发新邮件"模板,列出所有有效 Agent ID -3. 三条 ⚠️ 覆盖三种错误场景(自环 / 幻觉 agent / 状态误操作) -4. inform 模板也加上回复指引 +2. 新增"给其他人发新邮件"模板 +3. Agent ID 列表用 `{valid_agents}` 运行时替换(和 A4/A9 同一数据源),不硬编码 +4. 三条 ⚠️ 覆盖三种错误场景(自环 / 幻觉 agent / 状态误操作) +5. inform 模板也加上回复指引 #### 涉及文件 | 文件 | 改动 | |------|------| -| `src/api/mail_routes.py` | `send_mail` 加 A1-A8 校验 + 自动推断 | -| `src/daemon/spawner.py` | `MAIL_REQUEST_TEMPLATE` + `MAIL_INFORM_TEMPLATE` 更新 | +| `src/api/mail_routes.py` | `send_mail` 加 A1-A10 校验 + 自动推断 + 有效 Agent 列表 | +| `src/daemon/spawner.py` | 模板更新 + `{valid_agents}` 运行时替换 | #### 状态 -📋 设计完成,待评审 → 实施 → 验收 +📋 设计已纳入司马懿评审意见,待确认 → 实施 → 验收 ---