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

`reverseMap` 只在邮件投递时填 ⇒ 在 dsh 界面里直接对话时 `mailSessionOf(exec)` 为空
⇒ withScope「取不到就原样返回」⇒ 请求不带 session_id ⇒ 服务端 403
(AgentMayReadSession 收严后,2026-09-15 的迁移期放行已作废)。

不改的后果很具体:**在 dsh 界面里读不到任何信**。

回落答案只能向服务端问 —— dsh 侧给不出 session_id:
opencode 有宿主 session id 可反查(`sessionWorkspace`),pi 有 worker 闭包,
而 dsh 的 exec 里只有 dsh 自己的会话 id,与 AgentMail 会话 uuid 无映射。
这与 workspace 不同(workspace 能从目录/cwd 得到)。

装配期问一次(`loadDefaultSession`),不塞进 withScope:
withScope 是取值函数,IO 混进来就变成「拼 URL 时顺带发 HTTP」,
判据也无法用裸对象驱动(同 homeagent 那次的教训)。

问不到就当没有:不带 session_id → 服务端 403(可见的错误),
而不是静默放行成越权。

判据 4 格(接线形状 + 常量一致性 + 不得伪造 id + 失败要留日志)。
三个变异各红 3 格(判据之间交叉命中,说明它们各自都在钉不同的东西)。
tsc --noEmit 通过;桥全量 429/429 绿。
This commit is contained in:
2026-10-02 01:12:35 +08:00
parent de6b91516a
commit 86e05e7027
2 changed files with 108 additions and 1 deletions

View File

@ -381,6 +381,13 @@ interface PluginConfig {
export function apply(ctx: any, config: PluginConfig): void {
const GW = config.gateway.url || 'http://127.0.0.1:8180';
/*
* 默认落点必须与 handler.DefaultFallbackWorkspace 一致:
* 不一致 = 桥问的是<E79A84><E698AF><EFBFBD>个 key、服务端认的是另一个 ⇒ 永远问不到。
* 选 /tmp 的理由见服务端那个常量的注释(非邮件轮次没有真实工作目录)。
*/
const DEFAULT_FALLBACK_WORKSPACE = '/tmp';
const AGENT_NAME = config.gateway.agentName || 'dsh';
let AGENT_KEY = config.gateway.agentKey || '';
const AGENT_SECRET = config.gateway.agentSecret || '';
@ -393,9 +400,36 @@ export function apply(ctx: any, config: PluginConfig): void {
const client = new GatewayClient(GW, AGENT_NAME, AGENT_KEY, AGENT_SECRET);
/*
* ★ 2026-10-02:装配期问一次「我的默认会话」(非邮件轮次的 session_id 出口)。
*
* withScope 在 `mailSessionOf(exec)` 为空时用它兜底 —— 那发生在**界面里直接
* 对话**时(reverseMap 只在邮件投递时填)。服务端对未声明 session_id 的读信
* 一律 403(AgentMayReadSession 收严),所以不留这个兜底就等于
* 「在 dsh 界面里读不到信」。
*
* 那个端点是**纯只读**的:默认工作区一封都没通过时返回 session_id=null,
* 不建会话。所以这里拿到空是正常形状,不是错误 —— 后果只是那次读信 403,
* 而不是被静默放行成越权。
*/
let defaultSessionId = '';
const loadDefaultSession = async (): Promise<void> => {
try {
const r = await client.get(`/agent/session/default?workspace=${encodeURIComponent(DEFAULT_FALLBACK_WORKSPACE)}`);
defaultSessionId = String(r?.session_id ?? '');
if (defaultSessionId) {
ctx.logger.info(`[dsh-mail-bridge] 默认会话(/tmp): ${defaultSessionId}`);
}
} catch (e: any) {
// 问不到就当没有:不带 session_id,服务端会 403(可见)而不是静默放行。
ctx.logger.error(`[dsh-mail-bridge] 取默认会话失败(本次不带 session_id): ${e?.message || e}`);
}
};
// 注册 Agent
(async () => {
try {
await loadDefaultSession();
await client.register();
ctx.logger.info(`[dsh-mail-bridge] 已接入 ${GW},身份 ${AGENT_NAME}`);
} catch (e: any) {
@ -984,7 +1018,13 @@ export function apply(ctx: any, config: PluginConfig): void {
* 也不猜一个 —— 猜错会拼出投不到或投到别处的地址。
*/
function withScope(path: string, exec: any): string {
const sid = mailSessionOf(exec);
// ★ 2026-10-02:取不到邮件会话时回落到**默认会话**(/tmp),不再原样返回。
//
// 旧写法「原样返回」在 2026-09-15 之前是服务端放行的兜底(migration 期语义);
// 那个放行已作废(AgentMayReadSession 对未声明 session_id 一律 403),
// 而 dsh 的 `reverseMap` 只在邮件投递时才填 ⇒ 在界面里直接对话时
// `mailSessionOf(exec)` 为空 ⇒ 读信 403。
const sid = mailSessionOf(exec) || defaultSessionId;
if (!sid) return path;
const sep = path.includes('?') ? '&' : '?';
return `${path}${sep}session_id=${encodeURIComponent(sid)}`;