feat(opencode桥): withScope 在非邮件轮次回落到 /tmp 默认会话 —— 配套 15e4fe9 的收严

与 dsh 同一形状:`reverseMap` 只在邮件投递时填 ⇒ 用户在界面里新开一轮对话时
查表必空 ⇒ withScope「取不到就原样返回」⇒ 不带 session_id ⇒ 服务端 403。

实测它至今只裸奔过 1 次(2026-09-28),但**形状在**,且旧兜底的服务端放行已作废。
一致性比实测频率重要:留着一个已知失效的分支,下次有人读它会以为那是可用的。

回落答案只能问服务端 —— opencode 自己的 session id 与 AgentMail 会话 uuid
没有映射。★ 这与 `workspace` 那一维**不同**:那一维可以从 `Session.directory`
问出来(见 workspace-probe.test.mjs,同一天修的),所以这一维只能走默认会话。

装配期 fire-and-forget 问一次(不 await):它的失败后果只是那次读信 403,
不该拖住注册流程(而 dsh 那边是 await,因为 dsh 的注册路径本来就顺序执行)。
这个差异是有意的,不是抄漏。

判据 5 格。三个变异各红 3 格(交叉命中,说明各自在钉不同东西)。
全量 364/364 绿(359 + 5)。
This commit is contained in:
2026-10-02 01:15:58 +08:00
parent 86e05e7027
commit 85de9ee87b
2 changed files with 136 additions and 1 deletions

View File

@ -457,7 +457,17 @@ const downloadAttachmentTool = {
* (服务端会记警告),也不猜一个。
*/
function withScope(path, context) {
const mailSessionID = reverseMap.get(String(context?.sessionID ?? "")) || "";
// ★ 2026-10-02:取不到邮件会话时回落到**默认会话**(/tmp),不再原样返回。
//
// 旧写法「原样返回」是 2026-09-15 之前的迁移期兜底;那个放行已作废
// (AgentMayReadSession 对未声明 session_id 一律 403)。
// `reverseMap` 只在邮件投递时填 ⇒ 用户在 opencode 界面里新开一轮对话时
// 查表必空 ⇒ 读信 403。
//
// 回落答案只能问服务端:opencode 侧的 session id 是它自己的,
// 与 AgentMail 会话 uuid 没有映射(不像 workspace 能从 Session.directory 问出来)。
const mailSessionID =
reverseMap.get(String(context?.sessionID ?? "")) || defaultSessionId || "";
if (!mailSessionID) return path;
const sep = path.includes("?") ? "&" : "?";
return `${path}${sep}session_id=${encodeURIComponent(mailSessionID)}`;
@ -718,6 +728,42 @@ const sessionWorkspace = new BoundedMap(MAX_TRACKED_SESSIONS); // opencode sessi
还有宿主句柄 —— 以前没这个需求,所以没有通道。
*/
let hostClient = null;
/*
★ 2026-10-02:非邮件轮次的 session_id 兜底(/tmp 默认会话)。
`reverseMap` 只在邮件投递时填 ⇒ 用户在界面里直接对话时查表为空。
withScope 旧写法是「取不到就原样返回」,那是 2026-09-15 前的迁移期放行;
服务端已收严为「未声明 session_id 一律 403」⇒ 不加兜底就是界面里读不到信。
端点是**纯只读**的(默认工作区没通信过就返回 session_id=null,不建会话),
所以这里拿到空是正常形状;后果只是那次读信 403,而非静默放行成越权。
装配期问一次而不是每次 withScope 都问:后者会给服务端添无谓压力。
*/
const DEFAULT_FALLBACK_WORKSPACE = "/tmp"; // 必须与 handler.DefaultFallbackWorkspace 一致
let defaultSessionId = "";
let defaultSessionProbed = false;
async function loadDefaultSession(log) {
if (defaultSessionProbed || !hostClient?.session?.get) return;
defaultSessionProbed = true;
try {
const res = await fetch(
`${GATEWAY_URL}/api/v1/agent/session/default?workspace=${encodeURIComponent(DEFAULT_FALLBACK_WORKSPACE)}`,
{ headers: authHeaders() },
);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
defaultSessionId = String(data?.session_id ?? "");
if (defaultSessionId) {
log?.(`[mail-bridge] 默认会话(/tmp): ${defaultSessionId}`);
}
} catch (e) {
// 问不到就当没有:不带 session_id ⇒ 服务端 403(可见)而非静默放行。
log?.(`[mail-bridge] 取默认会话失败(本次不带 session_id): ${e?.message || e}`);
}
}
const syncedTitles = new BoundedMap(MAX_TRACKED_SESSIONS); // opencode session id -> 已回写过的标题(去重,避免 session.updated 刷屏)
// 权限询问的双向定位。
@ -1258,6 +1304,10 @@ export default async function mailBridge(input) {
// 供模块级工具对象(read_inbox)问工作区用,见 hostClient 的注释。
if (client) hostClient = client;
// ★ 2026-10-02:装配期问一次默认会话(非邮件轮次的 session_id 兜底)。
// 不 await:注册流程不该被它拖住(问不到的后果只是那次读信 403)。
loadDefaultSession((m) => console.log(m));
// 注册 Agent。无密钥也无 secret 时先本地生成一把密钥,
// 等管理员在后台登记后即可接入(无需重装插件)。
if (!AGENT_KEY && !AGENT_SECRET) {