`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 绿。
67 lines
3.3 KiB
JavaScript
67 lines
3.3 KiB
JavaScript
/**
|
||
* dsh:`withScope` 在**非邮件轮次**也必须声明一个合法 session_id(2026-10-02)。
|
||
*
|
||
* ## 为什么
|
||
*
|
||
* 服务端 `AgentMayReadSession` 收严后(`15e4fe9`),未声明 session_id 的读信
|
||
* 一律 403。而 dsh 的 `reverseMap` **只在邮件投递时**才填 ⇒ 在界面里直接对话时
|
||
* `mailSessionOf(exec)` 为空。
|
||
*
|
||
* 旧写法是「取不到就原样返回 path」,那在 2026-09-15 之前是服务端的迁移期兜底;
|
||
* 那个放行已作废。所以不改 = **在 dsh 界面里读不到任何信**。
|
||
*
|
||
* 回落答案只能向服务端问:`session_id` 是 AgentMail 会话的 UUID,
|
||
* dsh 侧的目录/会话 id 给不出它(这一点与 opencode 不同 —— 它有宿主 session id 可反查)。
|
||
*
|
||
* ## 为什么默认落点是 /tmp
|
||
*
|
||
* 与服务端 `handler.DefaultFallbackWorkspace` 必须一致,不一致 = 永远问不到。
|
||
*/
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const src = readFileSync(join(HERE, '..', 'src', 'index.ts'), 'utf8');
|
||
|
||
test('★ withScope 在 mailSessionOf 为空时回落到默认会话', () => {
|
||
assert.ok(
|
||
/const sid = mailSessionOf\(exec\) \|\| defaultSessionId;/.test(src),
|
||
'withScope 必须有默认会话回落(否则界面里直接对话时读信一律 403)');
|
||
assert.ok(
|
||
!/function withScope\(path: string, exec: any\): string \{\s*\n\s*const sid = mailSessionOf\(exec\);\s*\n\s*if \(!sid\) return path;/.test(src),
|
||
'withScope 仍是「取不到就原样返回」—— 那是 2026-09-15 前的迁移期兜底,服务端已不再放行');
|
||
});
|
||
|
||
test('默认会话在装配期问一次(不是每次 withScope 都问)', () => {
|
||
assert.ok(/let defaultSessionId = '';/.test(src), '缺少 defaultSessionId 缓存');
|
||
assert.ok(/await loadDefaultSession\(\);/.test(src), '装配期应调 loadDefaultSession');
|
||
assert.ok(
|
||
!/withScope[\s\S]{0,400}await\s+(client\.get|fetch)/.test(src),
|
||
'withScope 不该自己发请求:它是取值函数,IO 混进来会让「拼 URL 时顺带发 HTTP」,' +
|
||
'也让判据无法用裸对象驱动');
|
||
});
|
||
|
||
test('★ 默认落点与服务端一致(不一致 = 永远问不到)', () => {
|
||
const m = src.match(/const DEFAULT_FALLBACK_WORKSPACE = '([^']+)'/);
|
||
assert.ok(m, '缺少 DEFAULT_FALLBACK_WORKSPACE 常量');
|
||
assert.equal(m[1], '/tmp',
|
||
'必须与 handler.DefaultFallbackWorkspace 一致;不一致则桥问的 key 与服务端认的 key 不同');
|
||
});
|
||
|
||
test('问不到默认会话时不得编一个 id(那是把「不知道」记成「知道」)', () => {
|
||
const m = src.match(/const loadDefaultSession = async[\s\S]*?\n \};/);
|
||
assert.ok(m, '未找到 loadDefaultSession');
|
||
const body = m[0];
|
||
assert.ok(
|
||
/String\(r\?\.session_id \?\? ''\)/.test(body),
|
||
'应把缺失的 session_id 当空串(端点是纯只读的,没通信过就没有)');
|
||
assert.ok(
|
||
!/defaultSessionId\s*=\s*['"`][0-9a-f-]{36}['"`]/.test(body),
|
||
'不得硬编码或伪造一个 session_id');
|
||
assert.ok(
|
||
/catch[\s\S]*?ctx\.logger\.error/.test(body),
|
||
'问不到要留日志(否则「默认会话一直拿不到」这件事不可见)');
|
||
}); |