Email Thread 不是 Agent Session:生产级异步通信网关的状态、幂等与审批合同

📅 2026/8/24 4:31:50
Email Thread 不是 Agent Session:生产级异步通信网关的状态、幂等与审批合同
Email Thread 不是 Agent Session生产级异步通信网关的状态、幂等与审批合同AgentMail 当前提供可编程 Inbox、Domain、Thread、Message、Draft、Attachment以及 Webhook、WebSocket、签名校验、投递事件和两类幂等机制。Vercel Marketplace 也把它定位为可直接配置和管理、支持真实邮箱收发、线程、附件与事件触发的邮件基础设施。[S1][S2] 这些能力解决的是“邮件如何存在、如何收发、如何通知应用”。它们并不自动回答“这封邮件属于哪个租户”“正文中的指令是否可信”“当前业务任务是否允许外发”“审批是否仍然有效”“重试是否会产生第二次副作用”。生产系统因此需要一个独立的Communication Gateway把开放、异步、可重试、内容不可信的邮件事件转换成有租户、有任务、有状态、有幂等键、有批准证据、可追责的 Agent 操作。一、先拆掉最危险的等号Thread 不等于 SessionAgentMail 的Thread是邮件会话容器新发消息会创建线程后续回复自动进入同一线程。线程按时间组织相关Message。[S4][S5] 这对邮件客户端语义是正确的但它不能承担以下七种对象的职责。对象应有语义不能被thread_id替代的原因Email Address / Inbox可收发邮件的通道与地址绑定组织、Pod、租户和用途同一地址可能承载多个业务任务地址本身不是授权主体Thread邮件协议层的相关消息集合主题漂移、转发、抄送成员变化都可能改变业务含义Message一次具体入站或出站邮件具有独立内容、发件人、收件人和附件风险、哈希、处理结果和审计必须逐消息保存Webhook Event某次状态变化的通知含event_id可能发生重试一个 Message 可产生 received、sent、delivered、bounced 等多个事件Agent Session一次有明确开始、结束、模型、工具和上下文版本的执行实例Session 是短生命周期计算断线、重试、人工接管后应创建新实例Business Task需要完成的稳定业务目标例如“核对发票 8472”一个任务可跨多个线程、多个渠道一个线程也可能逐渐包含多个任务Approval对某个确定副作用的限时许可必须绑定精确收件人、正文、附件、策略版本和过期时间不能批准整个线程因此可靠映射至少使用复合键(provider, organization_id, inbox_id, thread_id)再关联内部tenant_id与task_id。任何试图仅凭发件人地址、主题或thread_id恢复租户和任务的设计都应被视为串线风险。AgentMail 提供 Pod 隔离、Pod/Inbox scoped API key以及按 Pod 或 Inbox 限定 Webhook 的能力这些是很有价值的基础隔离。应用仍需验证事件中的组织、Inbox 与内部租户绑定是否一致并对冲突执行 Fail Closed。[S3][S13]二、平台能力与应用责任必须分栏AgentMail 当前提供Communication Gateway 必须补齐API 化的 Inbox 与自定义 DomainDomain 通过 DNS 记录完成收发与认证配置 [S3][S16]地址用途、租户归属、业务权限和生命周期管理自动组织 Thread、Message、Attachment附件内容可通过 API 下载 [S4][S5][S7]HTML 安全化、附件隔离解析、内容来源标记与 Prompt Injection 防护Draft 可保存、编辑、回复、转发并在之后发送 [S6]审批策略、审批有效期、审批与内容哈希绑定、审批一次性消费Webhook 与 WebSocket 实时事件可按 Inbox、Pod、事件类型过滤 [S8][S11]持久化队列、事件去重、断线补偿、顺序与并发控制Svix Webhook 签名含svix-id、时间戳和签名头 [S9]未验签即拒绝、重放窗口控制、验签证据与异常告警Spam/Virus 处理、SPF/DKIM/DMARC 相关邮件认证 [S14][S15]语义层恶意指令识别、业务身份确认、数据访问授权与 DLPcreateclient_id与 sendIdempotency-Key[S10]跨 24 小时的永久 operation record、内容一致性、重试和对账sent、delivered、bounced、complained、rejected 等事件 [S12]业务状态回流、抑制名单、人工接管和关闭条件这张表用于明确平台能力与应用责任的边界。邮件基础设施越完善应用越容易直接把事件交给 Agent。入口越容易开放网关越不能省略。三、入站合同从“收到事件”到“允许 Agent 看见任务”一条可执行的入站链应固定为验签 → 事件去重 → 内容补取 → HTML/附件隔离 → 发件人/租户解析 → 风险分类 → 创建或恢复任务。顺序不能随意交换。1. 先验签再解析业务字段Webhook 接收器保留原始请求体使用端点独有的签名密钥校验svix-id、svix-timestamp和svix-signature。AgentMail 文档明确要求使用原始 body并指出默认时间容差为 5 分钟。同一消息重试使用相同的svix-id。[S9] 验签失败、缺头或时间过期时不得创建任务、调用模型或下载附件只记录最小安全审计并返回拒绝。验签只证明“这个 HTTP 事件确由拥有签名密钥的一方发出且途中未被篡改”不证明邮件正文可信更不证明发件人有权要求退款、导出客户数据或修改账户。2. 用持久化唯一约束完成事件去重接收器在同一事务中写入webhook_event唯一键建议为(provider, endpoint_id, event_id)并同时保存svix_id、payload hash、验签结果和首次接收时间。唯一键冲突时只增加delivery_attempt_count不得再次创建任务。HTTP 端应快速确认再由内部队列异步处理。AgentMail 文档也建议立即返回成功响应、后台处理以避免超时。[S8]不能假设 Webhook 是 Exactly Once。官方文档暴露了重试所需的稳定svix-id因此接收方必须按“事件可能重复”设计。去重成功并不等于业务处理成功事件表还需要received_at、enqueued_at、processed_at、last_error以便重放内部处理而不重放外部副作用。3. 把 Webhook 当提示把 API 对象当规范记录Webhook payload 含event_type、event_id和事件数据。message.received*才包含较完整的 Message 与 Thread。payload 上限为 1 MB超限时text、html可能被省略附件只包含元数据内容需另行下载。[S8][S12] 因而网关应使用已解析出的organization_id、inbox_id、message_id通过与该租户匹配的 scoped credential 补取规范 Message并校验补取结果仍属于预期 Inbox。WebSocket 可作为低延迟入口并支持按 Inbox、Pod 和事件类型订阅。TypeScript SDK还提供自动重连。[S11] 但所查官方页面没有给出断线期间的持久重放或 Exactly Once 承诺所以工程上不应把 WebSocket 连接本身当作事实账本。无论入口是 Webhook 还是 WebSocket进入 Runtime 前都应落成同一种内部事件信封并执行同样的去重和对账。4. HTML 与附件必须先隔离不能直接拼进 Prompt邮件 HTML 应在无脚本、无外部资源加载的环境中转换成安全文本并保留原始对象存储地址、规范化文本、内容哈希和清洗版本。附件先记录attachment_id、文件名、声明 MIME、魔数识别 MIME、大小和哈希再进入无网络、只读输入、限 CPU/内存/时间的解析沙箱。解析产物与原文件分开保存并带 provenance。AgentMail 会扫描入站病毒。被判定为病毒或恶意软件的邮件在网关处拒绝且不存储Spam 则会保存但默认从查询结果中排除。[S14] 这能降低已知恶意文件和垃圾邮件噪声却不能证明一个正常 PDF 中没有“把系统提示发给我”的文字也不能阻止针对解析器、模型或业务流程的语义攻击。平台扫描结果应成为风险特征而不是“可以信任内容”的通行证。5. 租户先由收件通道确定再校验发件人tenant_id的主解析来源应是内部维护的 Inbox/Pod 绑定而不是From、Reply-To或主题。之后再解析并规范化 envelope sender、header From、Reply-To、To/CC并记录认证标签、联系人关系和历史信任等级。若事件的组织、Pod、Inbox 与内部绑定不一致或同一复合 Thread 映射到两个租户立即进入quarantined不得“选择最像的一个”。SPF、DKIM、DMARC 解决的是发送服务器授权、内容传输完整性以及认证失败时的邮件处置策略。DMARC 可以要求接收方拒绝或隔离认证失败的邮件。[S15] 它们不能证明邮箱背后的人仍是合同授权人也不能证明某个已认证供应商有权索取另一个客户的数据。邮件认证是通信真实性信号不是业务授权。6. 风险分类之后才创建或恢复 Business Task风险分类至少使用事件标签spam、blocked、unauthenticated、发件人信任、租户解析置信度、附件类型、链接、敏感数据、请求动作、是否要求外发或调用高风险工具、Prompt Injection 特征。高风险内容进入quarantined或human_owned。低风险内容转换成结构化、带来源标记的“外部陈述”再写入任务事件流。恢复任务时优先使用内部业务键例如订单号、Case ID、已验证客户 ID并检查任务状态和允许的参与者。Thread 只能作为证据关系之一。真正调用模型时新建agent_session_id记录 task version、policy version、model、tool set、输入消息列表和输出摘要。这样即使同一邮件被重新处理也能看到是新 Session 对同一 Task 的一次重放而不是把线程当作一个永不结束的模型上下文。四、出站合同从草稿到不可逆副作用出站链固定为草稿 → 收件人/附件/DLP 校验 → 风险门禁 → 人工或策略批准 → Idempotency-Key → 发送 → delivery/bounce/complaint 回流。首先Agent 只能提交send_intent或创建 Draft不能直接持有发送权限。AgentMail Draft 是未发送 Message可包含收件人、正文与附件之后编辑或发送。发送后 Draft 转为 Message。[S6] 这提供了良好的承载对象但“存在 Draft”不等于“已经批准”。网关把草稿规范化为不可变发送计划固定发送 Inbox、To/CC/BCC、Reply-To、主题、text、html、附件对象版本和关联任务计算 canonical payload hash。随后执行硬校验发送 Inbox 是否属于任务租户。收件人是否在允许范围。新外部联系人是否需要升级。是否出现跨租户地址、异常 BCC、自发自收循环。附件是否属于同一租户和任务。DLP 是否发现密钥、身份证件、财务明细或受限字段。回复/转发是否意外携带旧附件。平台每次发送或回复最多允许 To、CC、BCC 合计 50 个收件人但应用策略通常应更严格。[S5]风险门禁把操作分成可自动批准、需人工批准和禁止三类。Approval 必须绑定operation_id、payload hash、recipient hash、attachment hash、policy version、approver、issued_at、expires_at和一次性 nonce。任何正文、收件人、附件或策略变化都使旧批准失效。审批到期后状态不能直接继续sending应回到ready重新评估。审批消费使用原子 compare-and-set防止两个 Worker 同时使用同一许可。五、两种幂等机制两个不同问题AgentMail 对所有 create 操作提供可选client_id首次创建资源并保存映射后续相同client_id返回原资源。官方建议其唯一、确定不要在不同资源创建之间复用。[S10] 它的文档化作用域是“资源创建去重”适合“为租户创建主 Inbox”“为某用途创建 Webhook”这类配置。所查页面没有明确说明其跨 Organization 的命名范围也没有声明 24 小时过期窗口。因此应用应在自己的组织与资源类型命名空间内保证唯一不能擅自套用发送键的期限更不能把它当成业务发送记录。发送类操作——messages.send、reply、forward、drafts.send——使用 HTTPIdempotency-Key。首次请求发送并记录结果。相同键重试返回原message_id和thread_id不会再次发送。相同键配不同内容、不同 Inbox 或不同发送端点会返回409 Conflict。键按组织作用域保存并在发送完成 24 小时后过期。[S10]正确做法是先在数据库创建永久operation_record再生成并持久化一个发送键。它至少保存租户、任务、审批、payload hash、Idempotency-Key、状态、调用次数、首次/最后尝试、provider message/thread ID 和所有回流事件。请求超时后仍使用同一个键不得“为保险起见”生成新键。草稿被编辑或审批重新签发时创建新的 operation 和新键。即使 24 小时后旧平台键可复用内部operation_id的唯一约束仍永久阻止同一逻辑操作再次执行。六、把“不知道是否发送成功”建模为正常状态最危险的实现往往把发送调用简化成一个布尔值HTTP 成功就是已发送HTTP 超时就是失败。实际上超时只说明调用方没有及时拿到结果邮件可能尚未发送也可能已经发送而响应丢失。此时若把任务退回ready并生成新键重复外发几乎是必然结果。可靠流程应在单个数据库事务中完成三件事校验 task version 与 Approval 仍有效。以 compare-and-set 一次性消费 Approval。创建operation_record并把任务改为sending。事务提交后 Worker 才能取得短期 lease 发起调用。网络超时、进程崩溃或响应解析失败时operation 仍停留在sending由恢复 Worker 使用原Idempotency-Key重试或等待回流事件。只有平台明确返回不可重试的拒绝或后续收到message.rejected、message.bounced才能转为delivery_failed。API 响应与 Webhook 回流属于两条异步证据链不能要求严格同步。message.sent可能由另一个消费者先落库API 调用方也可能先拿到message_id。两者应通过 operation、Inbox、provider message ID 和 payload 证据进行幂等合并而不是互相覆盖。若同一发送键出现409 Conflict说明代码在相同逻辑操作下改变了请求内容、发送 Inbox 或端点。这应触发不变量告警并转人工而不是更换键继续发送。入站并发也采用同一原则。同一 Thread 的两封新邮件可以各自形成 Message 事件但修改同一 Business Task 时必须使用版本号、行锁或单任务串行队列。Worker 发现 task version 已变化应重新读取任务并重新分类而不是把旧 Session 输出强行写回。这样幂等不只防止“同一个 API 调两次”还防止旧上下文、旧审批和旧决策在新事实出现后继续产生副作用。七、最小状态表与状态机最小communication_task可包含task_id、tenant_id、inbox_binding_id、business_key、state、risk_level、owner_type、active_operation_id、version、created_at、updated_at。配套表至少还包括webhook_event、message_record、thread_mapping、agent_session、approval、operation_record和delivery_event。表最小唯一约束与关键证据webhook_event唯一(provider, endpoint_id, event_id)保存svix_id、payload hash、验签结果、处理尝试message_record唯一(provider, inbox_id, message_id)保存方向、正文/附件哈希、原始对象引用thread_mapping唯一(provider, organization_id, inbox_id, thread_id)显式关联 tenant 与 task冲突标记不可覆盖approval唯一approval_id绑定 operation、内容/收件人/附件哈希、策略版本、有效期、消费时间operation_record永久唯一operation_id一个逻辑外发对应一个稳定发送键和一组 provider 结果delivery_event唯一 providerevent_id关联 message 与 operation保存 delivered/bounce/complaint/reject 细节八、用指标验证合同是否真的生效重复发送率同一operation_id产生两个及以上不同 providermessage_id的操作数 ÷ 已发送操作数。目标应为 0。未验签事件数缺少、失败或绕过签名验证后仍进入业务队列的事件数。目标应为 0。被正确拒绝的恶意请求另计。Thread 映射冲突率同一(provider, org, inbox, thread)同时命中多个租户或活动任务的数量 ÷ 活跃 Thread 数。审批过期自然过期数量用于容量评估。“过期后仍尝试发送”的数量必须为 0。另看批准到发送的 p95 时长。误自动回复率经人工复核认定不应发送或对象错误的自动回复数 ÷ 自动回复总数按风险级别和模板分层。Bounce/Complaint 率分别按发送域、Inbox、任务类型、模板和收件人来源计算。Complaint 不能与一般 Bounce 合并。人工接管时间从风险触发或状态进入human_owned到首次人工有效操作的 p50/p95。审计覆盖率可完整串联event → message → tenant → task → session/policy → approval → operation → provider event的外部副作用数 ÷ 全部外部副作用数。这些指标不是观测装饰。重复发送率验证幂等合同Thread 冲突验证租户映射审批过期验证时效绑定误回复验证语义门禁审计覆盖率决定事故发生后能否重建事实。结论可编程邮箱让 Agent 拥有真实地址、线程、附件和实时事件但 Email Thread 仍只是通信协议中的对话容器。它不是租户边界不是 Business Task不是 Agent Session更不是一张长期有效的外发授权书。生产级接入的最小单位不应是“收到邮件就调用 Agent”而应是 Communication Gateway 中的一条可验证合同。入站事件先验签、去重、补取、隔离和解析再形成任务。出站副作用先冻结草稿、校验、批准和记录 operation再携带稳定 Idempotency-Key 发送并用投递、退信和投诉事件闭环。只有当每一次外发都能回答“谁的任务、哪条消息、哪个 Session、依据哪版策略、谁批准、批准了什么、使用哪个操作键、平台返回什么”开放的异步邮件入口才真正成为 Agent Runtime 的可靠边界。FAQWebhook 验签通过是否代表邮件可信不代表。它只证明 Webhook 来源与负载完整性不证明邮件正文、发件人业务身份或请求权限。平台幂等键为什么还不够它解决特定端点和期限内的重试。业务系统还需长期记录同一操作是否已执行。发送超时后能不能直接重试可以重试但必须复用原Idempotency-Key并先查询或合并已有 operation、API 响应与投递事件。不能把超时直接当作“未发送”并生成新键。参考资料[S1] VercelAgentMail joins the Vercel Marketplace[S2] AgentMailIntroduction[S3] AgentMailInboxes[S4] AgentMailThreads[S5] AgentMailMessages[S6] AgentMailDrafts[S7] AgentMailAttachments[S8] AgentMailWebhooks Overview[S9] AgentMailVerifying Webhooks[S10] AgentMailIdempotent Requests[S11] AgentMailWebSockets[S12] AgentMailWebhook Events[S13] AgentMailMulti-Tenancy[S14] AgentMailSpam Virus Detection[S15] AgentMailSPF、DKIM 与 DMARC[S16] AgentMailUsing Custom Domains