2026-10-05,比对 MCP 与插件接入能力时实测发现。
## 症状
独立 MCP 宿主读自己刚收到的那封信,得到:
这封信不在你当前所在的那条会话里:…你当前在会话 。要读它,…
`你当前在会话 。` 中间是空的。
## 根因:一个 reason 值管三种拒绝,文案却只有一句
`AgentMayReadSession` 用**同一个 `reason`** 覆盖三种情形:
① 没声明 session_id(scope == nil)
② 声明了别人的会话
③ 声明了但**不是参与方**(session_id 由请求方提供,自述不是事实)
而 `canReadSession` 无条件拼 `"你当前在会话 " + self + "。"`,
于是 ① 拼出那句残缺文案。
★ 为什么这句残缺文案比「不说」更糟:模型读到「你当前在会话 。」
会以为自己在一个**无名会话**里,于是转去猜/去搜 ——
而正确做法(从 new_mail 事件取 session_id)就在同一句话的后半段,
它却读不到前半段。
## 修法
按 `scope` 有无分句,**不给空值留位置**:
① → 「你当前**没有**声明自己所在的那条会话,所以无从判断这封信是不是你的。」
② → 「你当前在会话 00000000-…」(仍只报自己声明的那条)
②③ 故意不拆开:`session_workspace.go:171` 已经警告过「新增 reason 还要同步改
handler 的 switch,漏改就是 500」,而两者给模型的指引**完全相同**
(都是"去那条会话读"),拆开只增加分支不增加信息。
## 实测(真实请求,部署后)
① 不声明 → 「你当前**没有**声明自己所在的那条会话…」 无残缺 ✓
② 声明他人 → 「你当前在会话 00000000-…」(自己声明的那条)✓
★ 「会话 。」残缺句已消失
## 判据
`TestSessionScopeHintNeverLeavesEmptySessionSlot` —— 盯**结构**而非文案本身:
- 禁止再出现 `"你当前在会话 "+self+"。` 这种无条件拼接
- 要求按 `scope != nil` 分句
- 要求存在「未声明会话」那一句
两个变异都转红:退回无条件拼接 / 删掉「未声明」那句。
## 顺带确认:MCP 与插件的工具面已对齐
实测 `tools/list`(11 个)与 4 个插件的真实注册项做集合比对:
MCP 有、插件没有: 无
插件有、MCP 没有: 无
⚠️ 中途差点误判:grep 到插件里有 `permission_decision` 等 3 个词,
以为 MCP 缺 3 个工具。查下去 `permission_decision` 是 **mail_type**
(收到授权请求邮件后转给 worker),另两个只存在于 `src/pool.mjs`
和测试里,**从不暴露给模型**。
117 lines
4.4 KiB
Go
117 lines
4.4 KiB
Go
package handler
|
||
|
||
/*
|
||
会话作用域拒绝的文案必须**指出 id 从哪来**(2026-10-04 实测驱动)。
|
||
|
||
★ 为什么要这一格:
|
||
实测「独立 MCP 宿主 E2E」时,第一版客户端卡在这里 ——
|
||
它收到了 new_mail 事件、知道该读那封信,却**不知道该传什么 session_id**,
|
||
于是一封信都读不到,报的是:
|
||
|
||
这封信不在你当前所在的那条会话里:…你当前在会话 。
|
||
|
||
原文案说了「就在它那条会话里读」(去哪读),却没说 **id 从哪来**
|
||
(怎么去读)。缺的那半句正是宿主唯一缺的信息。
|
||
|
||
★ 判据盯两件事,缺一不可:
|
||
① 提到 session_id(名字本身)
|
||
② 指出**来源**(new_mail 事件的 payload / session/default 端点)
|
||
—— 只加「请传 session_id」等于把问题原样推回去,宿主仍不知道去哪拿。
|
||
*/
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestSessionScopeHintTellsWhereToGetSessionID(t *testing.T) {
|
||
b, err := os.ReadFile("agent_discovery.go")
|
||
if err != nil {
|
||
t.Fatalf("读 agent_discovery.go: %v", err)
|
||
}
|
||
src := string(b)
|
||
|
||
i := strings.Index(src, "这封信不在你当前所在的那条会话里")
|
||
if i < 0 {
|
||
t.Fatal("找不到会话作用域的拒绝文案")
|
||
}
|
||
// ★ 只取这一条 Error(...) 调用,不能按固定窗口往后截。
|
||
// 初版用「起点 + 2200 字符」,而**同一文件别处的文档注释**里也写了
|
||
// `/agent/session/default`(那是 `agentScope` 上方的说明)⇒
|
||
// 删掉提示后窗口里仍有它 ⇒ 判据假绿。
|
||
// 这与「变异必须确认真的应用」同族,但更隐蔽:变异应用了、判据却因为
|
||
// **看错了范围**而没反应。
|
||
seg := src[i:]
|
||
if end := strings.Index(seg, `Error(w, http.StatusForbidden, "无权访问该会话"`); end > 0 {
|
||
seg = seg[:end]
|
||
}
|
||
|
||
// ① 提到 session_id
|
||
if !strings.Contains(seg, "session_id") {
|
||
t.Error("★ 拒绝文案里没有 session_id —— 调用方不知道该传什么")
|
||
}
|
||
// ② 指出来源:事件载荷 或 默认会话语义端点
|
||
hasEvent := strings.Contains(seg, "new_mail")
|
||
hasEndpoint := strings.Contains(seg, "/agent/session/default")
|
||
if !hasEvent && !hasEndpoint {
|
||
t.Error("★ 只说了「要传 session_id」却没说**从哪拿** ——" +
|
||
" 那等于把问题原样推回去,宿主仍然读不到信(实测卡点)")
|
||
}
|
||
// ③ 不得泄露目标会话 id(安全边界:那是别人的会话)
|
||
if strings.Contains(seg, `self = (*scope).String()`) &&
|
||
strings.Contains(seg, "self+"+"\" 目标会话") {
|
||
t.Error("★ 不得把目标会话 id 拼进文案 —— 那是安全边界(只报自己所在的会话)")
|
||
}
|
||
}
|
||
|
||
/*
|
||
★ 「你当前在会话 。」—— 一个值管三种拒绝,文案却只有一句(2026-10-05)。
|
||
|
||
`AgentMayReadSession` 用**同一个 reason** 覆盖三种拒绝:
|
||
① 没声明 session_id(scope == nil)
|
||
② 声明了别人的会话
|
||
③ 声明了但**不是参与方**(session_id 由请求方提供,自述不是事实)
|
||
|
||
原文案无条件拼 `"你当前在会话 " + self + "。"`,于是 ① 拼出:
|
||
|
||
这封信不在你当前所在的那条会话里:…你当前在会话 。要读它,…
|
||
|
||
实测(独立 MCP 宿主读自己刚收到的那封信)确实读到这句。
|
||
残缺的话会让模型以为自己在一个**无名会话**里,转而去猜/去搜 ——
|
||
比不说更糟。
|
||
|
||
★ 这条判据盯的是**结构**:文案必须按 scope 有无分句,
|
||
而不是靠手测一次。
|
||
*/
|
||
func TestSessionScopeHintNeverLeavesEmptySessionSlot(t *testing.T) {
|
||
b, err := os.ReadFile("agent_discovery.go")
|
||
if err != nil {
|
||
t.Fatalf("读 agent_discovery.go: %v", err)
|
||
}
|
||
src := string(b)
|
||
|
||
// ★ 回归:不得再出现「无条件拼接 self」的写法 ——
|
||
// 它就是那句残缺文案的唯一来源。
|
||
if strings.Contains(src, `"你当前在会话 "+self+"。`) {
|
||
t.Error("★ 又用回「你当前在会话 \" + self」的无条件拼接 —— " +
|
||
"scope 为 nil 时会拼出「你当前在会话 。」,模型会以为自己在一个无名会话里")
|
||
}
|
||
|
||
// 替代写法必须按 scope 有无分句
|
||
if !strings.Contains(src, "if scope != nil {") {
|
||
t.Error("文案没有按 scope 有无分句")
|
||
}
|
||
if !strings.Contains(src, "你当前**没有**声明") {
|
||
t.Error("★ 没有「未声明会话」这一句的文案 —— " +
|
||
"调用方不声明 session_id 时读不到任何提示该传什么")
|
||
}
|
||
}
|
||
|
||
func min2(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|