# 项目路线图(跨包) > **本文只放「要同时改 ≥2 个包」的项。** 判据见 > [conventions.md § L0 / L1 的边界判据](./docs/conventions.md#l0--l1-的边界判据): > **改一个包就能做完 → 该包自己的 `ROADMAP.md`;要同时改 ≥2 个包,或要动 CI / 脚本 / 规范 → 本文。** > > 包内待办在 `packages//ROADMAP.md`(当前有:`docker` · `tty` · `codegraph`)。 > 缺陷按编号记在各包 `DEFECTS.md`,不在本文。 > 本文每一项在动手前先转成可验收条目(做完回填「落点 + 门槛」)。 > > **怎么读 `## 待办` 这一节**:它是**历史与待办混排**——**带 ✅ 的条目 = 整项已落地**,原文留在 > 原位作历史(目标、取舍、当时的理由都在正文里,搬走就只剩一句结论),落点与门槛见 > [§ 已完成](#已完成落点--门槛);**没带 ✅ 的才是活待办**。判据就这一个符号,**不数条数、也不写死 > 条数**(写了就会漂)。 ## 待办 ### 1. ✅ 统一安全围栏:把 docker 的加固口径同步到 tty / dsh-mcp > 自 `packages/docker/ROADMAP.md` 迁入(2026-09-25),原文照录: 与 docker 同款的 loopback 围栏加固(**docker D31/D32**)尚未同步到这两个包。 这里只记**结论**,不在公开文档里展开具体手法;要做的话直接对齐 `src/index.ts` 里那两个函数。 **为什么是 L0**:要同时改 `tty` 与 `mcp` 两个包,且口径必须与 `docker` 一致—— 三处各写一份必然漂。约定见 [architecture.md § 一条请求经过什么](./docs/architecture.md#7-一条请求经过什么)。 ### 2. 跳板机(ProxyJump / ProxyCommand)✅ > **全部落地**(2026-09-25,见 [§ 已完成](#已完成落点--门槛)与 > [docs/proxyjump-plan.md](./docs/proxyjump-plan.md) 的进度表):两包加 `jump` 规格并补齐四道 > 白名单、tty 四个连接点共用 `prepareSshConnect` / `attachSshTransport`、docker 池键并入 > 跳板机身份、导入解析 `ProxyJump`(含同文件别名);连接簿对话框的跳板机字段与「试连」的 > 跳板机维度同日做完(`client-src/jump-field.js` 纯模块 + 预览 33/33 + 真机冒烟)。 > > **`ProxyCommand` 随后补上(闸门版)**:tty settings 新增 `allowProxyCommand`(**默认关**), > 关着时携带代理命令的连接**明确失败、不退回直连**,`~/.ssh/config` 导入**永不**自动带入; > docker 用**同一个**开关(读 tty settings),池键并入代理命令摘要(不回显命令原文); > 真机冒烟 `proxycommand-smoke.mjs`(自写 `proxy-bridge.mjs` 当 `ssh -W %h:%p` 的原语) > 与两包各一套单测(tty 16 例 / docker 12 例)都进了 CI。 > > **仍未做**:只有多跳链(跳板机的跳板机)——明确不做,导入遇嵌套别名按「解析不出」处理。 > 边界与理由见 [docs/proxyjump-plan.md](./docs/proxyjump-plan.md) 第 6.1 节(闸门定案表)。 同一个根因,两个包各写了一遍,迁到这里合并(两段原文都保留): **docker 侧**(自 `packages/docker/ROADMAP.md` 迁入): > `buildConnectConfig` 从不设 ssh2 的 `sock`,tty 的连接簿也不支持跳板机条目; > 企业内网主机几乎都要过 bastion。短期至少做到:配了跳板机的目标连不上时给出明确文案, > 而不是 20s 后一句通用超时。 **tty 侧**(自 `packages/tty/ROADMAP.md` 迁入): > `ssh-config.ts` 明写忽略、`buildConnectConfig` 从不设 ssh2 的 `sock`;企业内网主机几乎都靠 > bastion。**短期至少做到**:导入时跳过依赖跳板机的块并提示,不要静默产出一条注定 20s 超时的 > 连接簿条目。*(复核:仍未做,`ssh-config.ts:9` 的行为没变)* **为什么是 L0**:两包各有一套连接构造(docker 的 `src/ssh-exec.ts` 与 tty 的 `src/ssh.ts`), 跳板机要一起做,否则「docker 目标能过 bastion、终端不行」这种半吊子状态比不做更糟。 ### 3. ✅ 面板端 i18n(方案 + 闸门 + 逐包搬运 10/10) > 自 `packages/docker/ROADMAP.md` 迁入,原文照录: 界面只有中文;`README.en.md` 与中文版手工同步(**docker D76** 那类「文档里写死计数」的漂移 已经去掉,但双份维护的风险仍在)。 **为什么是 L0**:全部 10 个插件的界面都只有中文,要改就得一次改完(或先定一套 i18n 落地方案)。 单包先做会造出两套互不兼容的写法。相关的文档语言策略见 [conventions.md § 文档分档](./docs/conventions.md#文档分档)(中英同结构 / 简单包免英文)。 > **2026-09-25**:按原文允许的「或先定一套 i18n 落地方案」落地了方案本身—— > [docs/i18n.md](./docs/i18n.md) + 静态闸门 `scripts/check-i18n.mjs`(含自检与 > vitest 用例、CI 与 pre-commit 接线)+ 最小接入样本 `env` / `kit-settings`。 > **逐包搬运仍是待办**(8 个包,字符串量最大的 `codegraph` 约 240 条), > 落点 / 门槛 / 搬运顺序与各包风险见下方[§ 已完成](#已完成落点--门槛) 第 3 项。 > > **⚠️ 上面两段是 2026-09-25 的原话,已经不是现状**(2026-10-18 补这一句,按 > [conventions § 文档分档](./docs/conventions.md#文档分档) 纪律 3:原话保留、只加标注): > `node scripts/check-i18n.mjs` 现在的输出是「已接入 **11/11** 个客户端半体」,没有剩余包; > 标题里的「10/10」与正文的「全部 10 个插件」「8 个包」都是加第 13 个包 `loglens` 之前的数。 > **要数字就现场跑闸门**,进度表在 [docs/i18n.md § 进度表](./docs/i18n.md#进度表)。 ### 4. ✅ `isConcurrencySafe` 未声明 > 自 `packages/docker/ROADMAP.md` 迁入,原文照录: 所有 `docker_*` 工具都被宿主当作独占而串行化,只读工具本可并行。 这是本仓库各插件的共性(tty / codegraph 同样未声明),要改建议一起。 **为什么是 L0**:原文自己就写了「要改建议一起」——三个包的 `tools.register` 调用点要一起加。 ### 5. 变更端点的信任模型:能力开关的宿主侧授权 ✅ > 自 `packages/docker/ROADMAP.md` 迁入,原文照录: 当前信任模型是 loopback + 同源证明(**docker D31/D32**);本机任意进程仍可直接读写配置 (它能先 `POST /config {allowMutations:true}`)。docker socket 等价目标主机 root,值得再收紧。 **为什么是 L0**:这不是 docker 一家的性质——**任何**走回环围栏的插件都接受本机任意进程的请求。 要收紧就得定一套全仓通用的机制(如一次性 token 交换),单包先做会在插件之间留下不一致的安全假设。 与第 1 项同属「安全围栏」这条线,**建议合并规划**。 #### 5.1 落地的是「只能降不能升」,不是 token(2026-09-25) **原文点名的攻击路径只有一条**:`POST /config {allowMutations:true}`。而一次性 token **打不到 它**——变更子路由名单(docker 的 8 条)里根本没有 `/config`,因为 `/config` 是插件被禁用后 **唯一**的恢复入口,token 化等于把用户锁在外面。所以本轮做的是能**真正堵住那条路径**、 且不破坏恢复入口的那一层: - **提权只认宿主侧环境变量**(`DSH_DOCKER_ALLOW_MUTATIONS` / `DSH_DOCKER_ALLOW_EXEC` / `DSH_TTY_ALLOW_PROXY_COMMAND`),且**进程启动时采样一次**; - **HTTP 只能关、不能开**:给 `true` → 400 + 文案说清「设哪个变量 + 重启宿主」; - 配置里的 `true` **不算授权**(它与 HTTP 写进去的值存在同一个存储里,分不出来源); - 实现只有一份([`packages/kit/src/capability.ts`](./packages/kit/src/capability.ts)), 客户端按快照里的 `*Granted` 把开关渲染成「点不动 + 说明怎么开」; - 约定与威胁模型边界写进 [architecture.md § 7](./docs/architecture.md#7-一条请求经过什么)。 > ⚠️ 上面那一条里「**提权只认宿主侧环境变量**」在 2026-09-26 被扩展成**两条通道**(并修好了 > 环境变量那条的采样源),见下 § 5.1.1;「进程启动时采样一次」这句仍成立,但它的理由变了 > (不再是「防运行期注入」这个权宜,而是「只认启动时继承的环境」这条规则)。 #### 5.1.1 就地提权:第二条带外通道(2026-09-26) > 机制推演、API 取舍与威胁模型**不在本文**(本文只放「落点 + 门槛」):全部在 > [docs/capability-elevation-plan.md](./docs/capability-elevation-plan.md)——§0 是对手表与 > 三条结论、§1 是不变量、§3 是 kit 侧落点、附录 B 说明为什么砍掉确认码通道、附录 C 是实施记录 > (含 API 改名与四处偏差)、附录 D 是 tty 接入、§9 是明确不做(OS 级同意的结论与落地状态见 > [docs/os-consent-plan.md](./docs/os-consent-plan.md),PR4 后置)。 **做了什么**:补上**免重启**的第二条通道——页内点开关 → 面板给出一条「在宿主上落地一个随机名 文件」的命令 → 宿主发现该文件即授权;顺带修掉 5.1 遗留的采样源问题(`.env` / `~/.dsh/env.yml` 从此明确不算授权)。通道实现只有一份(kit),docker 与 tty 两个插件共用;未授权时开关可点 = 发起提权(不再是点不动的 `disabled`)。 **落点**: - kit:[`capability.ts`](./packages/kit/src/capability.ts)(判定 + 文案:`bindCapabilitySources` / `capabilityGranted` / `capabilityGrantAt`)、[`grant-store.ts`](./packages/kit/src/grant-store.ts) (`capabilityPaths()` 只拼一次路径 / `GrantStore` / `sharedGrantStore`)、 [`elevation.ts`](./packages/kit/src/elevation.ts)(`createElevationManager` / `auditLoadedGrants`); - docker:`/api/dsh-docker/elevate{,/status,/revoke}`,三条**全 POST**且**逐条**进 `MUTATION_SUBROUTES`(那条判据是「精确子路径 + POST」,只写一条另外两条就是裸的);授权到达与 撤销都由宿主侧 `onGrantChange` 重算。客户端:逐能力状态行 + 「撤销宿主授权」入口。 - tty:`/api/dsh-tty/elevate{,/status,/revoke}` 同上(`allowProxyCommand` 也接上第二条通道)。 - 编号:kit **D07**(采样源)/ **D08**(落点带归属)/ **D09**(持久授权可见)/ **D11**(共享存储)/ **D12**(验收的授权落点隔离);docker **D141–D144**(重算、快照、字面 `**`、证明覆盖); tty **D68**(字面 `**`)/ **D69**(脆测试隔离)/ **D70**(启动期闸门未初始化);codegraph **CG64**。 **门槛**(本文件管「哪些用例 / 真机」):`packages/kit/test/capability.test.ts`(8 条:白名单值、 **进程内只采样一次**、文案两步齐全)、`packages/kit/test/grant-store.test.ts`、 `packages/kit/test/elevation.test.ts`;`packages/docker/test/config-route.test.ts` 的 5 条 (400 且不落盘、降权免授权、配置里的 true 无效、授权后接受)、 `packages/docker/test/elevate-route.test.ts`(含「不碰文件系统的任意 HTTP 序列都提不了权」的负向 性质);`packages/tty/test/proxy-command.test.ts` 的 21 条(含未授权 / 未启用**两条文案必须不同**、 路由 400、四道白名单往返)、`packages/tty/test/probe.test.ts`(探针按原因分开报)、 `packages/tty/test/elevate-route.test.ts`;docker route-smoke 62/62(含快照暴露授权);真机:下面那张 真机验收表 + `pnpm live-smoke`(18 条,两个实例的授权目录各自隔离 → kit D12)。 **升级影响(刻意如此,已写进两包 README)**:升级前靠界面打开的开关**会变成关**——要恢复 就在宿主侧设环境变量并重启宿主。这正是「HTTP 不能提权」的代价:分不出来源的 `true` 一律不算。 **真机验收(2026-09-26,真 DSH 宿主 + 真 HTTP 路由,不是假 ctx)**:临时 profile(从 test 复制、links 到本仓)起两个实例——无授权与带授权各一,逐条验过: | 场景 | 实测 | |---|---| | 未授权 + 配置里写着 `allowExec: true` | 快照 `allowExec=false`、`allowExecGranted=false`(**「配置里的 true 不算授权」在真宿主上成立**) | | 未授权 + `POST /config {allowMutations:true}` | **400**,文案点名 `DSH_DOCKER_ALLOW_MUTATIONS` 与「重启宿主」;`allowExec` / `allowProxyCommand` 同 | | 未授权 + 降权(给 `false`) | **200**(紧急刹车不依赖授权) | | 未授权 + agent 工具 | docker 只注册 11 个只读工具(`docker_action` / `docker_exec` 等不出现) | | 带授权实例 | 快照 `*Granted=true`、按配置生效;`POST …true` → **200**;工具清单多出 `docker_action`/`docker_image_*`/`docker_exec` | | 试连三种文案 | 未授权 → 「未获宿主授权…DSH_TTY_ALLOW_PROXY_COMMAND…」;授权但开关关着 → 「未启用…」;真跑一条失败命令 → 带子进程 stderr | | 浏览器真正加载的那份 client.js | 从真宿主拉下来核过:新键与新逻辑都在(`allowProxyCommandGranted` / `hint.proxyCommandNotGranted` / `allowMutationsGranted`) | 真机验收顺带挖出并修掉 **tty D66**(探针结算后仍被后到的事件覆写结果,导致「连接已关闭」 这种空话盖掉带 stderr 的那句)——它**纯 mock 测不出来**,已补真进程回归。 **这套验收已固化成脚本**:`pnpm live-smoke`([scripts/live-host-smoke.mjs](./scripts/live-host-smoke.mjs), 18 条断言:上面的表(含 A7b 那条顺序回归)+ agent 工具清单 + 绕开工具直打 `/action`、`/exec` 的门控 + 宿主正在服务的 `client.js` 含新键)。它复制两份一次性 profile 起两个真宿主(无授权 / 带授权),跑完删干净、 不碰任何既有 profile;是 [RELEASING.md](./RELEASING.md) 发布门槛 #3「真实装进 DSH 跑一遍」的自动化形态。**2026-10-03 起 CI 与发布闸也跑它**(挂载车道, 形态 `--bootstrap --strict --render --chrome-arg --no-sandbox`;此前是「本地门槛不进 CI」—— 那条判断被有意推翻,理由见本文件 § 已完成 第 6 项与 [conventions.md § 挂载车道](./docs/conventions.md#真机脚本与-ci-接线))。 **两台 CI 腿对应的机器上都是 18/18(2026-09-26 实测)**:Windows 11(SYSTEM 上下文)与 Ubuntu 24.04 各自一条命令跑完(`--bootstrap --strict`),全程不需要事先手工建 profile。 这一轮 Windows 腿挖出 3 条真缺陷(`where dsh` 的 POSIX shim、`fs.cpSync` 把 junction 展开成 124MB 真副本、**tty D67** 探针阶段顺序),全部已修并补了回归。 **干净机器上也能跑(2026-09-26 补)**:加 `--bootstrap` ([scripts/live-profile.mjs](./scripts/live-profile.mjs))——机器上没有「link 到本仓」的 profile 时,它从 dsh **自带**的 `web` 模板现场造一个(link 本仓的 docker/tty、写一层让「配置里写着 true」 成立的 patch、再用 `--dump-config` 自证插件真进了阵容),跑完与两份拷贝一起删。这样**两台 CI 腿 对应的机器(Parallels 的 Windows / Ubuntu VM)也能连宿主一起验**,而不只是打印 SKIP。 #### 5.2 一次性 token 仍未做(按需) 它**仍然有独立价值**,但只针对「**能到回环、读不到文件**」的隔离进程(沙箱应用、被拿下的 renderer):token 让盲发失效。若要做,先书面定三件事:① 威胁模型(谁被拦、谁不被拦—— 同用户全权进程本来就能读 `~/.dsh/.credentials.yaml`、能直接跑 `docker`,任何进程内机制都拦不住); ② `/config` 与恢复通道怎么办(token 化之后用户怎么把插件救回来);③ 桌面壳的例外。 按「全仓安全线一项」规划,而不是 docker 的附加项。 #### 5.3 三条已知限制(刻意不修,别当缺陷重报) **三条边界**(复核时确认,写在这里免得被当缺陷重报):① 运行期改 / 删授权文件不生效 (要重启才读到)——删文件当撤销是**容易误以为生效**的一侧,界面上的「撤销宿主授权」才是正路; ② 授权记录的 key 是**裸环境变量名**,不含插件身份(同名 env 的两个插件会共享一条授权;今天的 名字都带前缀,现实风险低);③ 没有「仅本次运行有效」档位(TTL / boot 计数),持久生效是刻意的。 三条都写进了 `grant-store.ts` 的「已知限制」。 ### 6. ✅ CI 与 `package.json` 的脚本双真相源 诊断正文在 [conventions.md § 真机脚本与 CI 接线](./docs/conventions.md#真机脚本与-ci-接线) (同一条 hermetic 脚本,CI 用写死路径、包内用 `smoke` 条目,改名只会红一处)。 **待批准**:收敛要动 `.github/`,按 [§ AI 协作边界](./docs/conventions.md#ai-协作边界什么改动要先问) 属于必须先问的动作。 **方案**:[docs/ci-scripts-plan.md](./docs/ci-scripts-plan.md)——现算 **14 处**写死路径(ci.yml 11 + release.yml 3,docker 那三条两个 workflow 各写一遍)、方案 A(workflow 调用包内条目,14 行 → 10 行) 与方案 B(不动 workflow 的守卫)、rename 面、CI 影响、验收与回滚都列在那里;批准后照它执行 ——**已批准并执行(2026-10-01)**,执行记录见该文 § 9;**已做**:见 § 已完成 第 6 项。 ### 7. ✅ `architecture.md § 2` 包清单缺守卫 归属规矩写着「任何档位的新包都要在 [architecture.md § 包清单](./docs/architecture.md#2-包清单) 加一行」,现算 12/12 满足,但**没有任何守卫**:第 13 个包进来时不会有人红。 `docs-index.mjs` 管的是 `docs/*.md` 的登记与包内 `ROADMAP.md` 的记录节位置,管不到 「workspace 包 ↔ 清单行」这一层。 **为什么是 L0**:要同时动 `scripts/` 与 L0 文档。**修法**:在 `docs-index` 里加一条断言—— 从 `pnpm-workspace.yaml` 现算包集合,与 `architecture.md § 2` 表格里的 README 链接逐一对账, 多一个少一个都报(配 fixture 反例:清单少一行必须红)。 **已做(2026-10-01)**:落点与门槛见 § 已完成 第 7 项(`docs-index` 判据 5:两边现算 + 三种漂法各一条反例)。 > **2026-10-18 复核**:上面正文里的「现算 12/12」是**加守卫之前**的读数。第 13 个包 `loglens` > 进来时这道守卫确实在工作——`node scripts/docs-index.mjs` 现在报「workspace 包 13 个; > § 2 包清单 13 行(判据 5 要求一一对应)」,两边一致;包清单那一行也早就补了。 > 所以本节已经从「待办」变成「已治」,只剩这条历史读数需要标注。 ### 9. ✅ 对齐 dsh-web:补上「运行驱动」的两条发现渠道 与 `zhu1090093659/dsh-web` 的 16 个 workflow 逐条对照后找到的两个**真实**的洞:**发包内容无人守** (`files` 漏一项,十三道闸门 + 1700 条用例全绿而 tarball 里真的缺文件)与**没有运行驱动的发现渠道** (coverage 棘轮 + 争用型 flake 的自动发现)。它那 16 个里 12 个是治理用(单人项目不适用), 结构上本仓已经不差——差的是后一条这一层。 **为什么是 L0**:要同时动 `scripts/`、`.github/`、根 `package.json` 与 L0 文档。 **已做(2026-10-03)**:落点与门槛见 § 已完成 第 9 项(9.1 发包内容守卫 + 9.2 nightly 两条车道)。 **追加(2026-10-04)**:对照 `ant-design/ant-design` 的 46 个 workflow 后,又找到两条**真实**的洞 ——**「写了测试但从没运行过」没人守**(放错目录 / 扩展名写成 `.spec.ts` 即静默跳过, 实测 113 文件 / 1768 用例全绿而那个文件从未执行),以及**两处版本声明可以静默脱节** (vitest 与 `@vitest/coverage-v8` 脱版后覆盖率照常出正常数字;workflow 里的 dsh CLI pin 漂到 cohort 外后两道闸门都绿)。落点见 § 已完成 第 9.5 / 9.6 项。 ant-design 那 46 个里 12 个是治理、其余多是社区规模才需要的(分片 / SHA 钉扎 / size-limit), **本仓该抄的只有前一条**;反过来「每个 job 都写 `timeout-minutes`」本仓做得比它好(它有 10 个 job 没写)。 ### 8. `docs/` 的活规矩与已落地档案混在同一层 现算(`wc -l docs/*.md`,2026-10-01 时点):`docs/` 11 份 2558 行。其中三份**已落地方案的执行记录** (就地提权 451 / 跳板机 270 / 面板端 i18n 252)合计 976 行 = **38%**;而每次改代码都要翻的活规矩 (`conventions` 419 / `agent-real-test` 290 / `architecture` 194 / `troubleshooting` 193) 合计 1096 行 = 43%。两组平铺在同一目录、同一张归属表里,新人打开的第一屏近四成是历史。 **为什么是 L0**:要动 `docs/` 结构与归属表。**这是可发现性问题,不是正确性问题**,不急; 真要动,方向是分层(例如 `docs/landed/`)而不是继续加规矩。 ### 10. 插件工具接入会话权限档位(tier gate) 会话权限档位菜单(仅可查看 / 工作区内修改 / 完全权限 / Auto review)目前只约束宿主自带的 bash / fs 工具;插件工具完全不受档位约束——`tty_run` / `tty_send` 是**无沙箱**的任意命令执行面 (注册只受插件总开关门控),模型用它跑 bash 被沙箱拒绝的同一条命令直接生效;docker 破坏档工具 授权一次后 AI 调多少次人都看不到。[capability-elevation-plan.md](./docs/capability-elevation-plan.md) §9 当年后置的「动作分级 / 逐次确认」就是这条。 **为什么是 L0**:机制只有一份(kit:档位解析 + 决策矩阵 + 监听器工厂),tty 与 docker 各出一份 工具分类表与接线——单包先做会造出两套档位语义。宿主侧接缝(`tools/pre-execute` 的 `ask` 决策、 `approval` 服务与 UI answerer、`sandboxPolicy.resolve`)已在 peer 下限 `0.1.7-rc.2` 齐备, 运行时实测见方案附录 A。 **状态**:**代码已落地并真机验证(2026-10-07,R1–R4 按推荐值决议)**——kit `tier-gate.ts`、 两包分类表与 `tools/pre-execute` 接线、对账守卫与单测、README / architecture §7 都已就位(实施 偏差见方案附录 B)。**真机验收:V1(部分格)、V2、V3、V4、V5、V8 与 docker 侧读写分级通过**(浏览器驱动 真 agent 回合跑的,逐项证据见方案 §7 实测记录),V0 事件级核对与 V6/V7(服务缺位构造、 撤销授权半边)留待维护者。决策矩阵、两包分类表与 R1–R4 决议都在 [docs/permission-tier-plan.md](./docs/permission-tier-plan.md)。编号未动(实施中挖出的缺陷按 各包序列、动手时先问)。 **2026-10-25 增量(会话中途切档 + 受限档真机验收)**:此前工具面只在 `agent/created` / `tools/change` 时重算,**中途切档纹丝不动**(执行期闸立刻按新档判,所以安全没问题,但那正是「用户刚升档、模型却还 面对旧工具面」和「降档后模型反复试已被拒的工具」的来源)。现在 kit 的 `attachTierRestrict` 订阅 `session/event` 的三个旋钮事件(`sandbox/mode` / `approval/policy` / `permission/preset`),按会话 id 找回 agent 重算一次,**两个方向都收**,合并到 microtask、只按 `event.type` 过滤(档位从服务现读, 不读载荷——`codegraph CG66` 的信封坑)。落点:`packages/kit/src/tier-restrict.ts`(7 → 17 条单测, 四个变异各杀 1-6 条)。**门槛**:真机脚本 `pnpm --filter @hyzyn/dsh-kit run tier-smoke` ([scripts/verify-tier-restrict.mjs](./scripts/verify-tier-restrict.mjs),真 cordis 根 + 真 `tools`/`sessions`/`sandboxPolicy` 服务 + **三个包的真实分档表**,10/10 通过——受限档 23 个工具里 一个 write/exec 与 `mcp__*` 都没有、升档回到 39、降档再回 23、另一个会话一个名字都没变)。 **2026-11-05 修正(裁剪面收窄到只读档)**:`shouldRestrictForTier` 的旧判据是 `mode !== 'danger-full-access'`——于是**默认档**(工作区内修改)下写 / 执行类工具整批消失。这不是「更安全」: `workspace-write + ask` 那一格在矩阵里是 `ask`(人一点就能执行),工具**能**成功,藏起来等于替用户改档; 而且它把用户逼到唯一另一条路——**零询问**的完全权限档(比逐次 ask 宽得多)。用户现场(tty D103)就是 「想在容器宿主机上跑一条 `sed`,默认档下 `tty_send` 根本不在工具表里」。现判据改成 `mode === 'read-only'`, 工作区内修改档**工具可见、每次调用照旧逐次 ask**;执行期闸一行未动。落点:kit `tier-restrict.ts` (+ 6 条单测)、tty / docker / mcp 三包的裁剪用例、`tier-smoke` 新增「中间档不裁」一条并把降档改用 `read-only`。理由与取舍见 [docs/permission-tier-plan.md](./docs/permission-tier-plan.md) §10 的修正记录。 同轮实测了此前只有散文的一条边界:**per-agent 挂载进 agent 自己那一层的工具不受前缀裁剪** (宿主只裁继承层),且把这类名字写进 `deny` 会被整体拒绝(`unknown global tool`)——所以未来 「MCP 按需挂载」的档位执行只能是**卸载**,不能是 `restrict`。 ### 11. ✅ kit elevation 的审计行改走 stdout(与 docker D162 同一课) **真机发现(2026-10-07,DSH 0.2.1-alpha.1)**:插件经 `ctx.logger.info` 打的行**既不出现在 宿主 stdout、也找不到落盘文件**——能看到的 `[dsh-docker]` 行全部是 `console.log` 调用点。 kit `elevation.ts` 的五种审计行(begin / grant / expire / revoke / `auditLoadedGrants` 的 load,即 kit D09 的可见性承诺)走的全是 `ctx.logger`,等于在真宿主上**无处可查**—— 「持久授权今天一开机就静默开着」恰恰是这些行存在要回答的问题。docker 的 capability-use 审计行已按同课改走 `console.log`(docker D162,出口取舍见 `packages/docker/src/audit.ts` 文件头);kit 的 tier-gate 日志出口本来就是 console(先例)。 **为什么是 L0**:改的是 kit 的共享模块(`elevation.ts` 的 `audit()` 与 `auditLoadedGrants()`), docker 与 tty 两个消费包的调用点与测试要同步;kit 版本一动,全仓的 kit 钉子(精确版本) 跟着走。 **状态**:**同日修复(2026-10-07,kit 0.5.2 / kit D14)**——`audit()` 与 `auditLoadedGrants()` 出口固定 `console.log`(前缀与格式不变;`ElevationOptions.logger` 保留给 removeFile 告警), `auditLoadedGrants` 去掉 logger 形参(docker / tty 两个调用点同步),三份 elevate 相关测试的 断言面改为 console 捕获;kit 台账记 **kit D14**(已修 14),conventions 范围表与守卫 fixture 跟到 D14,全仓 9 个 kit 钉子 bump 0.5.1 → 0.5.2。kit 修好后两类行同通道,docker README 的保留条件已删。 ### 12. ✅ 内网标识的专项排查 + 闸门 **起因(2026-10-18)**:用户截图里出现一串内网服务名,并要求「做一下专项排查」。先量了一遍: `gitleaks detect` 全树 **0 命中**——缺的不是密钥扫描,而是**公网 IP 闸门管不到的那半边**: 内网服务名、项目代号、登录账号、私网主机号。这一类此前**没有任何守卫**,所以同类问题在 `92f01eaf6`(2026-10-12 清过一轮)与 `768b7720b`(一周后又写回来)之间来回漂。 **为什么是 L0**:横跨 `loglens` / `docker` / `codegraph` / `search` / `tty` / `kit` 六个包, 外加 `scripts/windows/*` 与 `docs/`,并且要新增一道仓库级闸门——任何一个包单独做都收不住。 **状态**:**同日完成**——五类共 85 处换成中性取值;新增 [`scripts/check-internal-identifiers.mjs`](./scripts/check-internal-identifiers.mjs) (`pnpm internal-ids:check`,词表只存 `sha256`),配 22 条单测与四条逐条实跑的反例。 历史不动(用户明确「历史记录先不处理了」)。落点与门槛见 [§ 已完成第 12 项](#12--内网标识的专项排查--闸门)。 ## 已完成(落点 + 门槛) ### 1. ✅ 统一安全围栏:docker 的加固口径同步到 tty / dsh-mcp **落点**:加固档从 `packages/docker/src/index.ts` 的局部实现**整体上提到** [`packages/kit/src/http.ts`](./packages/kit/src/http.ts),新增四个导出: `isLoopbackAddress`(127/8 全段 + IPv6 映射)、`isLoopbackRequestStrict`(同步快路径 + 别名 DNS 确认、来源检查在 DNS 之前)、`hasSameOriginProof`(含桌面壳 Cookie 例外)、 `originProofHint`(403 成因摘要)。三个包改走同一份:`docker` 删掉自己那 150 行、`tty` 删掉 `isLoopbackHttp` 并收敛出**唯一**闸门 `gateRoute`(导出仅供单测)、`mcp` 换成 `routeGate`(导出仅供单测)。 变更端点的同源证明按**动作**而不是方法判定(读路由不要求证明,否则旧 Safari / 裸 curl 一起被挡):docker 走 `MUTATION_SUBROUTES` + 四条流;tty 走 `gateRoute(..., {mutation})` + 两张前缀子路由表;mcp 两条 POST(`/servers/save`、`/test`)。`/config` 三处一致地 **不在**证明名单里——它是插件被禁用后唯一的恢复入口。 **门槛**:`packages/kit/test/http.test.ts`(加固档逐条语义 + 拒绝分支 + D80 的「DNS 之前」 时序 + fail closed,DNS 用假实现不依赖网络);`packages/tty/test/route-gate.test.ts`、 `packages/mcp/test/route-gate.test.ts`(每包的拒绝分支负例);`docker` 既有的 12 个路由/流 用例在**不动一行测试**的前提下继续绿——那就是「行为一字未改」的证据(232 passed)。 ### 2. ✅ 跳板机(ProxyJump / ProxyCommand):单跳全部落地,只剩多跳链明确不做 **落点**:`packages/tty/src/ssh-config.ts` 的 `parseSshConfigDetailed()`(新增)识别 `ProxyJump` / `ProxyCommand` 并**整块跳过 + 回报块名**(`ProxyNone` / `none` 视为显式直连, 照常导入),同时把另外三种丢弃(通配 / 无 User / 超上限)也变成计数—— 后两个此前是静默的(见 **tty D62**)。路由 `GET /api/dsh-tty/ssh-config` 回这套结构, 浏览器半体把「N 条依赖跳板机未导入:名字」直接说给用户。文案侧:tty 的 `classifyError` / 探针 TCP 超时、docker 的 `SSH_TIMEOUT_HINT` / `describeExecError` / 连接簿条目缺失三处都点出「本版本尚不支持的正是这个成因」。 **门槛**:`packages/tty/test/ssh-config.test.ts`(7 条:跳过 + 点名、`none` 直连、大小写、 其余丢弃只计数、溢出报数、名单截断但计数准)、`packages/tty/test/ssh-config-route.test.ts` (真 HOME 下走一遍路由,含围栏不放松)、`packages/tty/test/host-smoke.test.ts` 与 `packages/docker/test/ssh-stream-budget.test.ts` 的超时文案断言。 **单跳完整实现已落地**(同日):`jump` 规格 + 四道白名单、tty 侧四个连接点共用 `prepareSshConnect`/`attachJumpSock`(终端 / SFTP / 隧道 / 探针一起过 bastion,不留半吊子)、 docker 侧 `poolKey` 并入跳板机身份并在 `disposeAll`/空闲回收/重连时成对关连接、导入把 `ProxyJump` 解析成结构化 `jump`(含同文件别名 / `user@host:port` / IPv6)。连接簿对话框新增 「跳板机」一段(一个 `[用户名@]主机[:端口]` 输入框,勾「使用独立凭据」才展开覆盖字段)。 原文里「两包同时可用」的口径至此两包都成立。 **`ProxyCommand` 也已补上(闸门版,同日)**:`spawn(command, {shell:true})` 的 stdio 经 `Duplex.from` 当 `ConnectConfig.sock`,`%h/%p/%r/%n/%%` 按 OpenSSH 同义展开且代入值走白名单 (含 shell 特殊字符就拒绝执行)。闸门 = tty settings `allowProxyCommand`(**默认关**): 关着时携带代理命令的连接**明确失败、不退回直连**,探针在阶段 0 就返回并点名开关; 导入**永不**自动带入;与跳板机同时配时按 OpenSSH 语义 **ProxyJump 优先**并记 warn。 docker 侧读**同一个**开关、池键并入 `|cmd:`(只并入摘要:命令原文可能含凭据)。 收尾点与跳板机同一批(目标只是借用 stdio),另有「AbortError 不当失败原因」「传输比 `exit` 先到时也要交出已攒到的 stderr」两处细节(见 plan 文档 §6.2)。 **验证**(全绿):`packages/tty/scripts/jump-smoke.mjs`(真 bastion 的 `direct-tcpip` + 真目标 sshd;J1 正向 / J2 跳板机密码错点名跳板机 / J3 目标不可达同时点名两跳 / J4 收尾无残留连接; 已接进 CI)、`packages/docker/test/ssh-jump.test.ts`(假 ssh2:拨号顺序、通道当 `sock`、 池键区分、失败关连接)、`packages/tty/test/jump-spec.test.ts`(白名单往返 + 拒绝分支)、 `packages/tty/test/ssh-config.test.ts`(别名 / IPv6 / 嵌套别名 / ProxyCommand 分别报数)。 **界面(同日)**:连接簿对话框新增「跳板机」一段(一个 `[用户@]主机[:端口]` 输入框, 勾「使用独立凭据」才展开覆盖字段)、条目行显示「⇢ 经 X」、「试连」结果把跳板机那一跳单列 (`ProbeResult.jump`);解析与回填是纯模块 `client-src/jump-field.js`(8 条单测), 预览夹具 33/33 场景正常,tty 五个冒烟 + integration 113 PASS 未受影响。 **`ProxyCommand` 的验证**:`packages/tty/scripts/proxycommand-smoke.mjs`(真机:自写的 `scripts/lib/proxy-bridge.mjs` 当 `ssh -W %h:%p` 的原语;P1 终端 / P2 SFTP / P3 闸门关着明确 失败且**没起进程** / P4 命令失败文案带 stderr 摘要 / P5 注入被拒且没落地 / P6 无残留子进程; 已接进 CI)、`packages/tty/test/proxy-command.test.ts`(16 例)、 `packages/docker/test/ssh-proxy-command.test.ts`(12 例)。 **仍未做**:只剩多跳链(跳板机的跳板机)——明确不做(导入遇嵌套别名按「解析不出」处理)。 方案与全部坑仍见 [docs/proxyjump-plan.md](./docs/proxyjump-plan.md)(第 6.1 节是闸门定案表)。 **顺路修掉的两处**(做 ProxyCommand 时挖出来的,编号 tty **D64 / D65**):连接簿对话框的 「连接(并保存)」出口此前**漏带跳板机**(填了却直连、存下来的条目也没有它——四个出口只改了 三个);SFTP 池条目在 `conn.on('close')` 里只摘出池、**没关跳板机**(目标断一次就漏一条 keepalive 养着的连接)。两处都已收口到公共收尾入口。 **刻意不做**:不做「只让 tty 能过 bastion、docker 不行」的半吊子(原文的判据:那比不做更糟); 代理命令不做「按平台各写一套转义」(改白名单拒绝)、不在导入时自动带入;不做多跳链。 ### 3. ✅ 面板端 i18n:方案 + 闸门 + 10 个包全部接入 **落点**:方案落在新文件 [docs/i18n.md](./docs/i18n.md)(规范片段 / 键名规范 / 目录放哪 / 范围纪律 / 进度表 / 六个已知陷阱),规矩的入口在 [conventions.md § 面板端 i18n](./docs/conventions.md#面板端-i18n)。实现只有一处: **宿主自己的** `@deepseek-ai/dsh-client-locale`(`ctx.locale`)——插件侧不建共享模块、 不加构建步骤、不读 `navigator.language`(三条理由都是实测约束,写在方案里)。 闸门 `scripts/check-i18n.mjs` 查三件人一定会漏的事:zh/en **键集一致**、**占位符一致**、 代码里 `t('…')` 用到的键**必须有定义**(第三条是主要理由:写错一个字母只会让界面露出键名, 没有任何测试会红)。另查命名空间 = 包名、空值、**重复键**(JS 后写覆盖前写,求值后看不出来, 必须数字面量)与「安装片段五点是否照抄」。接线:CI 的 ubuntu step(先跑 `--self-test`)+ `.githooks/pre-commit`。 首批接入(证明方案在「没有构建步骤」那 7 个包的多数形态上成立):`env`(14 条键)、 `kit-settings`(4 条键)——目录内联在 `client.js` 里,**刻意不建 `client-src/`**(一建就会让 仓库级「宿主地址来源」静态规则停止覆盖 `client.js`,方案里有实测依据)。 **门槛**:`node scripts/check-i18n.mjs`(全仓,已接入的必须全绿)+ `--self-test`(20 条判据); `scripts/test/i18n.test.ts` 25 条(每个判据都配反例:英文缺键 / 占位符只写一边 / 重复键 / 空值 / 注释里的 `t('…')` 不得误报 / `translate(` 与 `.translate(` 不得误命中 / 动态键只能告警 / 安装片段缺一处要点名),最后一组拿**真实语料**跑一遍并钉住「已接入的包不许退回未接入」。 **逐包搬运进度(2026-09-25):10/10 全部接入,合计 1636 条键。** | 包 | 键数 | 剩余中文字面量 | 备注 | |---|---|---|---| | `kit-settings` | 4 | 0 | | | `env` | 14 | 1 | 输入示例(语法示例不翻) | | `search` | 46 | 42 | 全是匹配/定位数据(keywords / 面板标题别名 / DOM 定位串) | | `profile` | 47 | 0 | 两条全角括号选项是「只数汉字看不见」的典型 | | `mcp` | 66 | 0 | HTML 串最多的一批 | | `prompt` | 67 | 0 | | | `rss` | 132 | 14 | BUILTIN_CHANNELS 数据(与宿主逐字镜像,builtinOf 还要用它匹配) | | `codegraph` | 204 | 0 | `pure.js` 的 4 个函数改成接 `t` 参数 | | `tty` | 339 | 5 | 目录放**模块作用域**;兄弟模块按参数注入 `t` | | `docker` | 450 | 35 | 4 条 console 日志 + 31 条「问 Agent」payload(**agent 面向,不翻**) | 每个包都跑过:`node scripts/check-i18n.mjs`(键集 / 占位符 / 用法覆盖 / 域白名单 / 无死键) + `pnpm -r typecheck` + 该包 vitest(全仓 63 文件 1063 条)+ 有构建步骤的重建产物 (`pnpm -r build && git diff --exit-code` 的产物闸门)+ 有 CI 冒烟的跑冒烟 (docker 44/44 + 61/61 + 72/72,tty probe-route/ssh/sftplimits/probe/integration 全过)。 **量、剩余中文的逐条归类、三条写法经验(匹配数据不翻 / HTML 串要 `esc(t(…))` / 值在句子里用 占位符)、目录放哪、以及预览与 smoke 夹具要补什么,都在 [docs/i18n.md](./docs/i18n.md)(数字由闸门现算,不写死)。** 活越干越多不是意外:这一轮顺手修掉了两类**围栏加固(第 1 项)漏下的真实回归**—— CI 真在跑的 `packages/tty/scripts/probe-route-smoke.mjs` / `integration.mjs` 与真机脚本 `scripts/verify-mcp-*.mjs` 打变更端点时没有同源证明(403),按浏览器同源 fetch 的形状补了 `sec-fetch-site: same-origin`。 ### 4. ✅ `isConcurrencySafe` 未声明 **落点**:`docker` 的 11 个只读工具(`docker_targets` … `docker_volumes`)与 `tty` 的 9 个 只读工具(`tty_list` / `tty_books` / `tty_stats` / `tty_capture` / `tty_screen` / `tty_expect` / `tunnel_list` / `sftp_list` / `sftp_read` / `sftp_tree`)声明 `isConcurrencySafe: () => true`; 五个 docker 变更工具与八个 tty 变更工具**刻意不声明**(宿主按独占处理,多声明一次就是 两个 `docker_action` 并发跑)。**2026-10-01 补**:`tty_run`(一次性命令)加入后 tty 的变更工具 是 8 个——新增工具必须来这两张表里归类(门槛那条「清单自洽」就是为此设的)。 **codegraph 那半边查下来不成立**:`packages/codegraph/src/**` 里一个 `tools.register` 都没有 (它只往 systemPrompt 注入两段 + 管 MCP 托管行)。模型看到的 `mcp__codegraph__codegraph_explore` 由宿主的 `@deepseek-ai/dsh-mcp-client` 注册,而那份 声明不在本仓,插件侧也够不到(没有改已注册工具的公开 API)。**要它并发安全只能改上游**, 本仓如实记录,不假装做了。 **门槛**:`packages/docker/test/tool-concurrency.test.ts`、 `packages/tty/test/tool-concurrency.test.ts`——三张表(只读 / 变更 / 清单自洽), 且断言走工具自己的参数校验(`defineTool` 参数不合法会直接返回 false,用 `{}` 调会把 「我们调错了」误读成「没声明」;测试按 JSON Schema 造合法最小实参)。清单自洽那条保证 **新增工具时必须来表里归类**。 ### 5. ✅ 变更端点的信任模型:能力开关的宿主侧授权 **落点**:机制在 kit——[`capability.ts`](./packages/kit/src/capability.ts)(判定与文案)、 [`grant-store.ts`](./packages/kit/src/grant-store.ts)(`capabilityPaths()` 只拼一次路径 / `GrantStore` / `sharedGrantStore`)、[`elevation.ts`](./packages/kit/src/elevation.ts) (`createElevationManager` / `auditLoadedGrants`);两个消费插件各接三条 `/elevate` 路由 (docker / tty)并在卡片上给出**逐能力**的状态行与撤销入口。方案、威胁模型与实施偏差见 [docs/capability-elevation-plan.md](./docs/capability-elevation-plan.md)(v1 已删除,冻结指针见下表)。 **门槛**:`packages/kit/test/{capability,grant-store,elevation}.test.ts`、 `packages/docker/test/elevate-route.test.ts`、`packages/tty/test/elevate-route.test.ts`; 真机 `pnpm live-smoke`(18 条,两个实例的授权目录各自隔离 → kit D12)+ 待办第 5 项下面那张 真机验收表。 ### 6. ✅ CI 与 `package.json` 的脚本双真相源:workflow 调包内条目(14 → 10 行) **落点**:`ci.yml` 三个 step 的 `run:` 与 `release.yml` 那一步改成 `pnpm --filter <包名> run <条目>` (tty 7 条 + docker 3 条并成一条 `smoke` + windows 1 条;docker 那三条**在两个 workflow 里各写了一遍**, 一起收敛);step 数、`if:`、矩阵一律未动。真相源收敛到 `package.json`。 方案、rename 面、CI 影响与验收见 [docs/ci-scripts-plan.md](./docs/ci-scripts-plan.md)。 **门槛**:两份 workflow YAML 仍可解析;`grep -c 'node packages/' .github/workflows/*.yml` = **0 / 0**; **机械等价 14 条**(HEAD 里每条写死路径都有同名条目、且条目命令逐字指向同一个脚本文件); 本地演练 8/9 个入口绿(`integration` 在本环境跑不起来——无法开 PTY,**旧写法同样失败**,属环境限制); **改名演练**:脚本改名 → 包内入口立刻红(`Cannot find module`),而同一时刻旧写法的写死路径照跑 ——那正是被消灭的静默腐烂;还原后 sha256 一致。 **未验证**:windows 腿的 `pnpm run`、CI 真跑一轮。 **方案 B(守卫)同轮已做**:`scripts/ci-script-truth.mjs` + 10 条用例(写死路径回归 / 条目改名 / 包名打错 / 发布闸跑 CI 不认的入口 / 同一入口两侧命令不一致 / 恒绿警戒);端到端反证——把写死路径 塞回 `ci.yml` → 守卫报 `ci.hardcodedPath`、4 条用例红,`cp` 还原后 sha256 一致并复绿。 **根级收尾(2026-10-03,[docs/ci-scripts-plan.md § 10](./docs/ci-scripts-plan.md))**:方案 A 当时只收了 **包内**条目;根 `scripts/` 那批发布不变量闸仍写死路径、**且 `ci.yml` 与 `release.yml` 各写一遍**。 本轮按同一套收尾——根 `package.json` 加 13 个条目(`aggregate:check` / `artifacts:check` / `publishable:check` / `dsh-peers:check` / `dsh-home:check` / `kit-pins:check` / `doc-links:check` / `issue-forms:check` / `i18n:check` / `no-public-ip:check` / `no-public-ip:message` / `release:publish`), 两条 workflow 一律裸调用 `pnpm <条目>`;`check-aggregate.mjs` 与 `check-artifacts.mjs` 落成脚本 (原 `cmd || { …; }` 组语法在 Windows cmd 下不成立,脚本里 argv 直传 git 顺带解决 `:(glob)` 语义)。 守卫加第 6 / 7 条判据(`ci.rootHardcodedPath` / `ci.rootEntry.missing`,pnpm 内建命令与注释行白名单), 用例 10 → 16 条。 **门槛**:8 道纯闸门条目逐条演练绿;`artifacts:check` 真实重建后 diff 通过;`aggregate:check` 用 临时 index 造「已提交」树后退出码 0(其红只对未提交状态反应,与原内联命令同语义);守卫两端反证 (塞回写死路径 → `ci.rootHardcodedPath` + 退出码 1 + 5 条用例红;`cp` 还原 → 退出码 0 + sha256 一致); 全量 **107 文件 / 1671 用例绿**。 **未验证**:actionlint step 本机没跑过(docker 拉不到镜像、无本地二进制,仅 YAML 解析 + 结构核查); Windows 腿的 `pnpm run` 形态。 **挂载车道(2026-10-03,P2;[conventions.md § 挂载车道](./docs/conventions.md#真机脚本与-ci-接线))**: 本轮补的是 CI 里最大的那个洞——9 道闸门全在验「这棵树自洽吗」,**没有一道会加载 `client.js`**, 而它才是用户实际运行的东西(`vitest.config.ts` 明写「打包后的 client.js 不在本层测」,tty D61 就是 这么漏掉的)。做法是**复用现成积木**而不是新写一套: `--bootstrap`(干净腿现场造 profile,早就为 CI 设计)+ `--render`(新增的 C 段,复用 `chrome-cdp.mjs` 的零依赖 CDP 客户端)+ `--strict`(没装 DSH 从 SKIP 变红)。CI job `mount-smoke`(ubuntu-only, 与三平台矩阵分开,不占它的时间)+ 发布闸同命令 step(成对,防 docker D119 那种「只在 CI 补」)。 **顺带修掉两处写死的 macOS Chrome 路径**:新增 `scripts/chrome-path.mjs`(平台候选表 + `CHROME_PATH` / `CHROME_BIN` + `--chrome` 点名),`chrome-cdp.mjs` 与 `verify-client-ui.mjs` 都改用它 ——原先的常量让任何非 macOS 调用方必然拿到不存在的路径。 **门槛**:本机真跑 **23/23 PASS**(18 条原有 HTTP 断言 + C1–C5 渲染断言),且是在**干净 `DSH_HOME`** 下跑的(复现 CI 的 `--bootstrap` 路径:现场造 profile → docker/tty 自证进阵容 → 两个实例) ——C2 证明本仓 client.js 真在浏览器里被加载。 **实测挖出两条**:① 受限环境必须 `--no-sandbox`(本机沙箱下不带它 CDP `Runtime.enable` 必超时、 带了 5/5 全绿)——这条直接进了 CI 命令;② 第一版守卫用全文 `toContain('--no-sandbox')` 会被 **注释里同一个词**满足 → 删掉旗标照绿(反例实测只红 1 条),改成只看 `run:` 命令行后同样的反例 红 2 条。守卫 20 → 28 条,全量 **108 文件 / 1692 用例绿**。 **未验证**:CI 上首次真跑(本机是 macOS + 已装 dsh 0.2.1-alpha.1;Ubuntu runner 的 Chrome 由 候选表首项 `/usr/bin/google-chrome` 覆盖,但那台机器的实际镜像只等首轮 CI 确认); `--no-sandbox` 在 GitHub 托管 runner 上是否必需(本机沙箱下必需,runner 上可能不必要但无害)。 ### 7. ✅ `architecture.md § 2` 包清单缺守卫:两边现算 + 三种漂法各一条反例 **落点**:`scripts/docs-index.mjs` 新增**判据 5**(`checkPackageManifest` / `parseManifestPackages` / `parseWorkspaceGlobs` / `resolveWorkspacePackages`)——**左右两边都现算**:左边从 `pnpm-workspace.yaml` 的通配解析出带 `package.json` 的目录,右边从 `## 2. 包清单` 那一节的 `../packages//README.md` 链接取包名。`readDocsInputs` 多带 `workspacePackages` 与 `architecture` 两份输入;CLI 多打一行「workspace 包 N 个 / § 2 包清单 M 行」(命中不是 0 要看得见)。 通配只支持本仓在用的 `dir` 与 `dir/*`,出现别的形态**抛错**——漏一批包就是恒绿,宁可炸。 `conventions.md § 插件包解剖` 那句「任何档位都要在 § 包清单加一行」补上了判据指针。 **三条口径**:workspace 有、清单没有 → `manifest.missing`(新包没登记);清单有、workspace 没有 → `manifest.stale`(删包没删行,清单成了历史);`## 2. 包清单` 整节被改名/删掉 → **报缺失**, 不静默当成空清单。只认那一节里的链接——文档别处的 `../packages/...` 引用不算登记。 **门槛**:`scripts/test/docs-index.test.ts` 的反例 5 六条——删一行 / 加一行假包 / 节被改名 / 把整张表清空(仍必须按 12 行报,不许被别处链接喂饱)/ 合成 workspace(少一个包 → stale)/ CRLF 免疫; 外加一条正向断言把「命中不是 0」钉住(两边都 ≥12、集合相等)。真实仓库 12/12 现算一致。 > **2026-10-18 复核**:上面那些 12 是落地当天的读数(第 13 个包 `loglens` 之前)。判据本身不写死数, > 现算现在是 **13/13** 一致;`docs-index` 每次跑都会打出「workspace 包 13 个;§ 2 包清单 13 行」。 > 按纪律 3 只加这一句标注,正文的旧数不改。 ### 9. ✅ 对齐 dsh-web:补上「运行驱动」的两条发现渠道 **背景**(2026-10-03 与 `zhu1090093659/dsh-web` 的 16 个 workflow 逐条对照):结构上本仓已经不差 ——它最强的三块(挂载车道、发布硬闸、产物一致性)本仓都有,端到端 smoke 还多出 8 套; 它那 16 个里 12 个是治理用(PR 证据门 / issue 去重 / reject-docs-pr / auto-assign / stale-assignment / contributors / labeler / deploy-market),单人项目不适用。**差的是发现渠道的性质**:本仓是 「闸门驱动」(静态一致性极强:13 道不变量闸 + 1700 条用例),而 dsh-web 多一层「运行驱动」 (真的 pack 安装、coverage 棘轮、flake 连跑)。对照中找出**两个真实的洞**,本轮都补上。 #### 9.1 ✅ 发包内容守卫(真打一次包,断言声明的入口都在 tarball 里) **为什么是真洞**(实测反证,不是理论风险):把 `packages/tty/package.json` 的 `files` 里 `"client.js"` 删掉后,`publishable:check` / `dsh-peers:check` / `kit-pins:check` / `aggregate:check` **全绿**、全量 **110 文件 / 1714 用例全绿**,而 `pnpm pack` 出的 tarball 里真的没有它—— 用户装上就是浏览器半体 404。三条既有车道各自都看不见:`artifacts:check` 只管「产物 vs 源码」、 `publishable:check` 只管 `workspace:` 残留,而**挂载车道走 `link:`(直接链到本仓目录), 绕过 `files` 字段**——dsh-web 那条用 `pnpm pack` + `file:`,天然覆盖。 这是**车道形态不同**带来的缺口,不是谁写漏了断言。 **落点**:[`scripts/check-package-contents.mjs`](./scripts/check-package-contents.mjs) (`pnpm package-contents:check`)——`pnpm pack --dry-run` 真的打一次包(本地、不落 tarball、 不联网,13 个目标各约 0.14s),判三条:① `main` / `types` / `exports` 每个值 / `dsh.bundle.patch` / 声明了 `dsh.client` 时的 `client.js` **全部从 manifest 现算**(写死清单就是 同一个 bug 换位置)必须在 tarball 里;② `files` 里的字面路径必须存在于磁盘(pnpm 对不存在的路径 **静默忽略**,实测加一条 `DOES-NOT-EXIST.md` 退出码 0、只是不包含它);③ 命中不是 0。 **接线成对**:`ci.yml` 与 `release.yml` 各一条(tag 可以打在没过 CI 的提交上)。 **门槛**:`scripts/test/package-contents.test.ts` 16 条;反证两端都实测过——删 `client.js` → 红并 点名 `exports["./client"]` 与 `dsh.client` 两条声明;`files` 里塞不存在的 `CHANGELOG.md` → 同样红。 `ci-script-truth` 的两条根级判据也覆盖了它(entry 名打错 / 只在 release 侧 → 各自红)。 #### 9.2 ✅ nightly 两条车道:并发 flake + coverage 棘轮 **落点**:[`.github/workflows/nightly.yml`](./.github/workflows/nightly.yml)(每天 15:15 UTC + 手动) + 两个根级条目 `pnpm flake:check` / `pnpm coverage:check`。**两者都不是「把 CI 再跑一遍」**: - **flake**([`scripts/flake-lane.mjs`](./scripts/flake-lane.mjs)):**并发**跑 N 份全量套件 (默认 3),红了**点名哪一份、哪条用例**。**刻意不照抄 dsh-web 的顺序三遍**——那抓的是顺序 / 状态污染型,而本仓三条间歇红(tty D95 / D57、codegraph CG65)是**争用 / 墙钟型**,实测顺序三遍 对它们从来全绿。并发 3 份 ≈28s 比顺序 3 遍 ≈42s **还便宜**。 **端到端反证**(真机校准,本机 10 核):空载 `spawnSync(node -e 0)` p50=25ms / max=33ms, 并发 3 份套件下 p50=39 / p90=97 / max=183ms;据此临时写一条「必须 <60ms」的赌墙钟用例 → **3 份里红 2 份、并点名了那条用例**(单跑 5 次全绿),验完删除。 - **coverage 棘轮**([`scripts/coverage-ratchet.mjs`](./scripts/coverage-ratchet.mjs)): **只统计宿主半体**(`packages//src`)——实测只算 `src` 是 **66.5%**、加上 `client-src` 掉到 **35.9%**,差 30 个点是因为浏览器半体的纯逻辑已抽成 `client-src/*.js` 由单测覆盖、 剩下那部分按定义跑在浏览器里(那半由挂载车道管)。基线冻在 [`scripts/coverage-baseline.json`](./scripts/coverage-baseline.json),只判「比基线低不超过 **0.3 个百分点**」(实测同树抖动 ≤0.01:66.52 / 66.53 / 66.53)。回退要用 `pnpm coverage:write` 显式接受——代价是**这件事出现在 diff 里**。 **门槛**:`scripts/test/flake-lane.test.ts` 14 条 + `scripts/test/coverage-ratchet.test.ts` 9 条 (含「零份 = 红」的恒绿警戒、「一份红 → 整体红并点名」、「json-summary 形态漂了必须硬红」、 「超订倍数与核数/份数无关地恒定」)。 真机反证:coverage 新增 122 行无人使用的源码 → 66.52 → **66.13(−0.39)被抓住**;flake 的 端到端反证见上。 **顺带**:`nightly.yml` 加进 `ci-script-truth.mjs` 的 `WORKFLOW_FILES`(入口同样不许漂), 但**不受**「发布闸不许跑 CI 不认的入口」那条约束——它存在的理由正是跑 CI 不该每次跑的东西。 `coverage` 需要 `@vitest/coverage-v8`,本轮加进根 `devDependencies`(此前本机有一份是残留、 **不在 lockfile 里**,`--frozen-lockfile` 的 CI 上装不到它)。 #### 9.3 ✅ 两条车道的真 runner 首跑(2026-10-04):全绿,并纠正两处推测 nightly 跑过两次(手动 `workflow_dispatch` + schedule 自动),**两条车道四次全绿**: | run | 触发 | flake | coverage | job 耗时 | |---|---|---|---|---| | `37133747178` | 手动 23:34 CST | ✅ 3/3 · 83.2s | ✅ 通过 | 1m42s | | `37145129043` | schedule 02:41 CST | ✅ 3/3 · 65.9s | ✅ 通过 | 1m22s | **结论:这条车道可以按天信任。** 本地 26–43s → CI 65–83s(约 2 倍,符合 10 核 vs 4 核)。 **顺带纠正两处此前写错的推测**(都记进了各自的归宿): 1. **覆盖率容差的理由不完整**。此前写「实测同树抖动 ≤0.01,余量给足」——那个量级只够支撑 0.05,**不足以**支撑 0.3。真正决定这个数的是 **CI 与本机的系统性差异**(两次运行逐位一致, 不是噪声):lines −0.03 / branches −0.04 / **functions −0.16**。若容差取 0.1,这条车道 **在 CI 上每次必红**。完整表与「想收紧前先看它」的警告已补进 [conventions.md § 车道分工](./docs/conventions.md#车道分工哪条车道管什么)。 2. **schedule 的延迟是小时级**。`nightly.yml` 原注释抄了「延迟可达几十分钟」,实测 **晚 3 小时 26 分**(槽位 23:15 CST → 实跑 02:41 CST)。同时确认「只认已存在于默认分支的 workflow」——本文件 22:48 进 main,23:15 的槽位没赶上。两条都进了 workflow 注释与 conventions。 #### 9.4 ✅ flake 车道的争用强度不再随机器漂(2026-10-04) 不加 `--maxWorkers` 时 vitest 取「核数 − 1」,于是「并发 3 份」在本机(10 核 → 27 进程 = 2.7x)与 CI(4 核 → 9 进程 = 2.25x)的**争用强度不同**,车道的灵敏度不可比——本机绿不代表 CI 绿,而那正是它要回答的问题;副作用是本机跑起来瞬间冒出近三十个 node 进程(会被当成泄漏)。 改成**从核数反算每份 worker 数**,让总超订恒定为 2 倍:本机 21 进程 / 2.10x、CI 9 进程 / 2.25x。取 2 而不是历史上复现用过的 5:这是每天跑的常设闸门,不该长期占满开发机 CPU。 门槛:新增 3 条用例钉住这个不变量(含「下限是 1,`--maxWorkers=0` 是非法值」)。 #### 9.5 ✅ 测试收集守卫:写了的测试不许从不运行(2026-10-04) **来源**:对照 `ant-design/ant-design` 的 46 个 workflow 时,在 `test.yml` 里看到一步 「数磁盘上的测试文件 vs `vitest list --filesOnly` 收集到的数,差值写进 Job Summary」。 它做的正是「**写了但从没运行过**」这一类检查——本仓此前**没有任何闸门守这一层**。 **洞(实测反证)**:`vitest.config.ts` 的 `include` 是写死的两条 glob,所以「放错目录」或 「扩展名写成 `.spec.ts`」是**静默**的。把一条必然失败的断言放进 `packages/tty/tests/zz-probe.spec.ts`: ``` vitest 收集 → 0 个 · pnpm test → 113 文件 / 1768 用例全绿 · 十二道闸门无一变红 ``` 那个文件**从未执行过一次**。与已记在案的 **tty D61**(逻辑埋在 `client-src` 里 = 没有测试入口)、 **v0.1.20 的 dsh-docker 漏进 `publish-targets.mjs`** 是同一形状:**「该有的东西找不到」不会报错**。 **落点**:`scripts/check-test-collection.mjs`(`pnpm test-collection:check`),三条判据—— ① 盘面(`git ls-files --cached --others --exclude-standard`,`--others` 让**未 `git add`** 的 新文件也被看见)减去收集结果、减去白名单必须为空;② 收集结果不是 0;③ 白名单不许腐烂。 盘面 glob 刻意**比 include 宽**(认全 `*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs,mts,cts}`)—— 照抄 include 就抓不到「`.test.ts` 写成 `.spec.ts`」这个最常见的漏法。 **白名单一条**(设计,非漏):`templates/hello/test/hello.test.ts` 是 `create-plugin` 的模板, 复制进 `packages//` 后才被收集;模板自己待在 `templates/` 下刻意不收集。 **接线**:`ci.yml` + `release.yml` 成对;发布闸那边放在 `pnpm test` **之前**。 **门槛**:`scripts/test/check-test-collection.test.ts` 13 条用例(核心反例 = 两种漏法) + `--self-test` 8 组夹具;四种反例实测都能红(放错目录 / `.spec.ts` / 未 `git add` 的新文件 / 白名单腐烂),真仓库 114 盘面 − 113 收集 − 1 豁免 = 0 违规。成本 **0.32s**。 #### 9.6 ✅ 工具链钉子守卫:两处版本声明必须自洽(2026-10-04) **来源**:与 9.5 同一轮——对照 `ant-design/ant-design` 时顺带把本仓「声明层」自查了一遍, 发现两处「A 处版本必须与 B 处一致」的约束**没有任何闸门守**。两条都逐条实测过脱节后果。 **洞 ①:vitest 与 `@vitest/coverage-v8` 脱版**。改成 `3.2.0` 后:`pnpm install` 装成功 (pnpm 10 默认不拦 peer,本仓没开 `strict-peer-dependencies`)、`dsh-peers:check` / `kit-pins:check` / `package-contents:check` / `artifacts:check` 全绿、`coverage:check` **用 3.2.0 跑完并出正常数字**。而 coverage-v8 的 peer 是**精确** `vitest: "3.2.7"`, 源码里**无任何版本自检**(grep 过 `dist/`)。这是「覆盖率引擎被换掉而报告一模一样」。 **洞 ②:workflow 里的 dsh CLI pin 漂移**。改成 `0.1.6-alpha.2`(真实存在、不在 cohort 里) 后 `dsh-peers:check` 与 `kit-pins:check` 都绿。诚实说这条**不会假绿**(`--bootstrap` 会自证 失败),但**症状指向「挂载车道坏了」而不是「pin 漂了」**——闸门的价值是缩短定位路径。 **落点**:`scripts/check-toolchain-pins.mjs`(`pnpm toolchain:check`)。判据 ① 三条 (实装同版 / coverage 的 peer 被满足 / 声明必须精确),② 两条(pin 在 cohort 里 / 必须是最新档)。 cohort 列表从**根 peer 范围**拆(`check-dsh-peers.mjs` 是纯 CLI、不可 import),这条耦合由 用例当场钉住。**刻意不引 semver**:根上解析不到,且本仓对预发布范围的匹配最易出错 (`check-dsh-peers.mjs` 文件头记了 `^0.1.7-rc.2` 展不开 `0.2.0-rc.1` 那个坑)。 **接线**:`ci.yml` + `release.yml` 成对;发布闸那边放在 `npm install -g dsh@` **之前**。 **门槛**:24 条用例 + 18 组夹具;六种反例实测都能红(脱版 / peer 不符 / 声明写成范围 / pin 不在 cohort / pin 落后一档 / `@latest`),只改 `release.yml` 也报。 **一个真事**:这条闸门**第一次跑就红了**——抓到的是**我写在它自己 step 注释里的示例** `npm install -g @deepseek-ai/dsh@`(`` 被当成版本号)。修法 = 排除注释行 (`isCommentLine`,与 `ci-script-truth.mjs` 同名函数同一条教训)。同一个 bug 也被单测里 「真实仓库的 pin 是最新 cohort」那条抓到——**闸门实现与用例侧各写一遍正则,两边踩同一个坑、 也就两边验证了修法**。 #### 9.7 ✅ 官方判定器核对进 CI:用宿主真正会跑的那段代码复核声明(2026-10-04) **来源**:用户问「有校验样式是否符合 DSH 规范的 lint 吗」。查证结论是**官方没有 lint 命令** (`dsh` 只有 boot / plugin;`dsh-package-manifest` 是**纯类型包**,`lib/index.js` 只有 11 字节 `export {};`,README 原话「Each reader owns JSON parsing, validation, and default resolution」)。但查出本仓**早就有一处官方校验器的接线却从没进 CI**。 **洞(性质与前面几条不同——这不是「修 bug」,是「把靠人记得的手工步骤自动化」)**: `check-dsh-peers.mjs --app-boot ` 这条路径 2026-09-29 就有,历史证据是 `packages/codegraph/README.md` 记着两轮发布前**手工**跑过、逐 cohort × 逐包核对。但它 **从没进 CI**——每次要人手把路径拼出来当参数传。这正是 `conventions.md` 那条 「CI 与发布闸要成对」的反面。 **为什么值得接**:`dsh-peers:check` 的默认路径只回答「**我的声明符合我的规则**吗」 (范围逐字等于 `EXPECTED_RANGE`、`manifestVersion` / `engines.dsh` 齐全)。而「这个 cohort **到底放不放行**」只有官方能回答——`evaluatePluginCompatibility` 是宿主**安装前与启动时 真正跑的那段代码**。实测两者**不重叠**:官方那个 44 行函数**不看** `dsh.manifestVersion` / `dsh.bundle.patch` / `dsh.engines.dsh`。 **落点**:新增 `scripts/dsh-app-boot.mjs`(自动定位,四级优先:`--app-boot` > `$DSH_APP_BOOT_DIR` > 从 `which dsh` 真身上溯 > `npm root -g` 兜底;后两条各兼容 **vendored** 与 **hoisted** 两种 npm 布局);`check-dsh-peers.mjs` 加 `--with-app-boot`。接线在 `ci.yml` 的 `mount-smoke` job(它已装 pinned dsh),放在 `Install dsh CLI` 之后、`live-smoke` 之前。 **两条纪律**(都有用例钉住):① `--app-boot` / `$DSH_APP_BOOT_DIR` **点了名就只认它**, 指错直接失败、不回落——实测过不这么做的后果是 env 被静默忽略、自动定位成功、结论照绿; ② 找不到就**报错退出**、不静默跳过,并列出试过哪些路径。 **门槛**:`scripts/test/dsh-app-boot.test.ts` 26 条用例(含两种 npm 布局、三级优先级、 「点了名就只认它」、未装 dsh 时 tried 为空)。实测:本机定位到 `dsh-bin` 来源、13 包 × 4 cohort 全放行;真正不含 dsh 的 PATH 下报错退出(exit=1);`DSH_APP_BOOT_DIR` 指错时报错而非回落。 ### 11. ✅ kit elevation 的审计行改走 stdout(与 docker D162 同一课) **落点**:[`packages/kit/src/elevation.ts`](./packages/kit/src/elevation.ts) 的 `audit()` 与 `auditLoadedGrants()` 出口固定 `console.log`(行前缀与格式不变,代码注释落 **kit D14**; `ElevationOptions.logger` 保留给 removeFile 的 best-effort 告警);`auditLoadedGrants` 去掉 logger 形参,docker / tty 两个调用点同步瘦身;kit 台账记 **kit D14**(已修 14 / 待修 0), conventions 范围表与 `defects-table` 守卫 fixture 跟到 D14;全仓 9 个 kit 精确钉子 0.5.1 → 0.5.2(aggregate + lockfile 同步);kit README 的 API 签名与通道表述、docker README 中英「两类行同通道」的收口同步。 **门槛**:kit `test/elevation.test.ts` 18 条、docker / tty 各自 `test/elevate-route.test.ts` (断言面从注入 logger 改为 **console 捕获**——捕到的就是真宿主 stdout 上会出现的字节, 这一步同时让测试与真机行为对齐:此前这些断言消费的是 `ctx.logger`,而真宿主上它不可见); `defects-table` 守卫 33 条(kit 两段范围 D06–D14 现算 + 反例针更新)。 ### 12. ✅ 内网标识的专项排查 + 闸门 **起因(2026-10-18)**:用户截图里出现一串内网服务名,并要求「做一下专项排查」。先量了一遍: `gitleaks detect` 全树 **0 命中**(密钥层面是干净的)——缺的不是密钥扫描,而是**公网 IP 闸门 管不到的那半边**:内网服务名、项目代号、登录账号、私网主机号。这一类此前**没有任何守卫**, 所以同类问题在 `92f01eaf6`(2026-10-12 清过一轮)与 `768b7720b`(一周后又写回来)之间来回漂。 **为什么是 L0**:横跨 `loglens` / `docker` / `codegraph` / `search` / `tty` / `kit` 六个包, 外加 `scripts/windows/*` 与 `docs/`;并且要新增一道仓库级闸门(`scripts/` + `package.json` + `conventions.md`),任何一个包单独做都收不住。 **落点**:五类共 **85 处**换成中性取值——内网服务名(56 处,含 `loglens/src/tools.ts` 那条 **agent 直接读得到的 tool description**)、项目代号与真实业务类名(6 处,在 `codegraph/docs/adoption-audit.md` 的「调用质量举证」里连 Java 类名一起贴了)、本机用户名 (10 处)、Parallels 网段与账号(6 处)、登录账号(13 处)。脱敏口径沿用 `92f01eaf6` 的先例 (服务名 → `app-svc-N` / `batch-job-a`,主机名 → `host-a.internal`,账号 → `operator`, 本机路径 → `/Users/you`,地址 → RFC 5737 文档网段);**夹具的搜索词与名字是一对**,必须成组换 (`'ems'` 前缀命中改成 `'app'` 前缀命中),否则用例要么变红、要么变成假绿。 四个 `scripts/windows/*.mjs` 原先把内网地址与账号写成**默认值**(`--ssh-host` / `--ssh-user` / `--ssh-root`),改成「不给就整档 SKIP 并说清怎么给」——这正是 `a2150d347`(2026-10-03)给 devdata / devenv 立过的规矩,那轮漏了这批脚本。 新增闸门 [`scripts/check-internal-identifiers.mjs`](./scripts/check-internal-identifiers.mjs) (`pnpm internal-ids:check`,另有 `--staged` / `--message-file` / `--self-test` / `--hash`), 接进 `package.json` 与 [conventions.md § 提交](./docs/conventions.md#提交)。两个设计决定: **词表只存 `sha256`**(闸门自身不含明文,否则等于换个地方泄露);**判据是「是不是那批已知 真名」而不是「像不像敏感」**(公开产品名与泛化账号词不进表——一道总在报假警的闸最后会被绕过)。 **门槛**:`scripts/test/internal-identifiers.test.ts` **22 条**(切段 / 边界 / 大小写 / 注入 / 真实语料 / 「闸门自身不含明文」)。四条反例逐条实跑变红: 1. 片段预算被掐掉(`> TOKEN_MAX` 改成 `> 0`)→ 11 条红; 2. **默认词表被摘掉**(`banned ?? BANNED_HASHES` → `new Set()`)→ 自检红(`锚点词应命中 1 处, 实得 0 处`)。这条是补的:第一版自检**抓不到**它——所有用例都显式注入临时表,于是「默认表 被清空」时自检全绿、全仓扫描也照样「通过」,闸门静默失效。修法是在词表里留一条**人为造的 假词**当锚点(写法见脚本里的 `ANCHOR_WORD`),专门反证「默认这条路接上了」; 3. 超长 token 整条跳过(旧实现的盲区)→ 新用例红,改成「按**片段**算预算」; 4. 边界改松成子串匹配 → 粘连变体上误报,反例用例红。 全仓 **3125/3127**(两条红是既有的 `os-consent-scope` 版本基线,与本改动无关);typecheck / build / i18n(11/11 客户端半体)/ doc-links(644 条)/ 公网 IP 闸门全绿。`artifacts:check` 与 `aggregate:check` 都是对 HEAD 做 diff,须在提交后判定。 **接线(同日,用户批准后补)**:`.githooks/pre-commit` 扫 **index**(`--staged`,放在重建之后 ——产物里的注释也要被扫到)、`.githooks/commit-msg` 扫**提交信息**(写消息时「引用现场」把真名 抄进去是最常见的泄露路径)、`.github/workflows/ci.yml` 两个 step 兜底(`pnpm internal-ids:check` +按推送区间扫消息,口径照抄公网 IP 那两条)。两处钩子都实测过反例:带真名的提交信息 `exit=1` 并点名「提交信息含真实内部标识」;暂存区带真名的文件 `--staged` 也 `exit=1`。 **没做的(如实记下)**:历史未处理(用户明确「历史记录先不处理了」——本仓是公开仓,已公开的 内容不做重写;清理历史只能重写,代价远高于入口拦一次)。 ### 2026-10-25:MCP 工具面按需加载(`agentToolLoading`) **落点**:`packages/mcp` 新增配置项 `agentToolLoading: 'eager' | 'on-demand'`(**默认 `eager`**, 零行为变化);宿主半体新文件 `src/agent-surface.ts`(纯函数 `planSurfaceDeny` + `attachAgentSurface`,结构照 kit 的 `tier-restrict`:`agent/created` / `agent/disposed` / `tools/change` 三条订阅、microtask 合并、全程不抛),`apply` 里注册两个自有工具 `mcp_servers`(read:列 server、工具数、是否已装载)/ `mcp_load`(exec:装 / 卸并回报该 agent 视图里真实的 `mcp__*` 名字)。档位那条不变(自有工具接进同一套裁剪,两条限制是**交集**)。 README 中英、[`docs/agent-real-test.md`](./docs/agent-real-test.md) 的入口表与 `scripts/verify-list.mjs` 同步。 **为什么不做 per-agent 进程挂载**:那是 codegraph 的隔离路线(每会话一个子进程 ≈40MB、要动 patch 行格式与卡片)。本轮要解决的是 **token 账单**:真机会话的 `request/header` 快照实测 149 个工具 130,439 B,其中 MCP 70 个 66,362 B(≈50.9%、≈1.7 万 token/请求,**本机单次测量、 不是承诺值**)。所以工具照旧由宿主注册,`restrict()` 只把它们从该 agent 的**模型视图**里摘掉 ——请求体收益与「不注册」等价,**不解决进程隔离**(那是另一个问题,别混为一谈)。也不走 `deferLoading`:这条路由上基线工具没有激活路径(实测见当轮调查记录)。 **门槛**:`packages/mcp/test/agent-surface.test.ts` 14 条(含两条变异反例:把「属于已装载 server」的 判据去掉、把「只裁托管行」的收口去掉,各自对应的用例必须红);`pnpm --filter @hyzyn/dsh-mcp exec tsc --noEmit`、 `… build`、`pnpm vitest run packages/mcp/test`;真机 [`scripts/verify-mcp-on-demand.mjs`](./scripts/verify-mcp-on-demand.mjs)(进程内真宿主 + 真 `dsh-mcp-client` 挂真 stdio 探针服务器 + 真插件):`on-demand` 初始视面无 `mcp__probe__*` 且 装载前真调用被拒、`mcp_servers` 报 `on-demand` / `tools: 2` / `loaded: false`、 `mcp_load(['probe'])` 后**真调用** `probe_echo` 拿到回显、另一个 agent 的工具面逐名未变、 `mcp_load([])` 卸干净 + 未知 server 不装不抛、`eager` 第二宿主零副作用、真实补丁逐字节未变。 **边界(明确不做 / 还没做)**:默认**仍 `eager`**(翻不翻是维护者的判断,见下一条的真机回合 结论);不改动 `mcp__*` 的宿主注册方式与 patch 行;不解决进程隔离(每会话一个子进程那条是 codegraph 的路线,另开)。 ### 2026-10-25:`agentToolLoading` 的**真模型回合**验收(默认仍 `eager`) 脚本能证明「工具面与装载动作对」,证明不了模型的**选择**——所以补了一次真宿主 + 真模型 + 真浏览器的 回合,判据取自**会话日志**(不是模型的自述): - **做法**:复制一份真实 profile 到临时名(真 `~/.dsh`,凭据/路由都在)→ `--patch` 覆盖两处 (`webserver.port` 换空闲口、`mcp-config.agentToolLoading: on-demand`)→ `dsh --profile <临时> --patch /tmp/… --no-open` → 浏览器把访问模式切到「完全权限」→ 发一句「先看有哪些 MCP server、 挑一个工具数大于 0 的装载起来,实际调一个工具」。收尾:关页面、停宿主;**真实 home 补丁 mtime/size 未变**;临时 profile 未删(删除要先问)。 - **结果(`tool/call` 三条,按序)**:`mcp_servers({})` → `mcp_load({"servers":["dbx"]})` → `mcp__dbx__dbx_list_connections({})`。1 轮 4 步。 - **工具面证据(`request/header` 快照)**:装载前 **81 个工具**,`mcp` 族只有 `mcp_servers` / `mcp_load`(+ codegraph 那条 per-agent 挂载的 `mcp__codegraph__*`,它不在本插件托管行里,按 「只裁托管行」**本就不该被裁**);`mcp_load(['dbx'])` 之后的请求 **106 个工具**,多出来的正是 dbx 的 27 个,**其余 server 的工具仍然不可见**。同机 eager 基线实测是 149 个 → on-demand 在 装载前省掉 **68 个工具(−45.6%)**。 - **附带实测**:这一轮里把访问模式从「工作区内修改」切到「完全权限」,日志里落了 `permission/preset` ×2 + `sandbox/mode` ×2——即 2026-10-25 那条「会话中途切档重算」在**真 UI** 上走通了;同一份日志也把 per-agent 挂载的 own-layer 豁免(`mcp__codegraph__*` 在受限档仍可见) 在真宿主里复现了一次。 ### 2026-10-25:`DOCKER_GUIDANCE` 拆段(562 → 250),进 250 名单 - **落点**:`packages/docker/src/index.ts`(公告本体)、`packages/docker/test/announcement.test.ts` (判据手术)、`scripts/announcement-budget.mjs` 的 `ANNOUNCEMENT_NAMES`/`PACKAGES`(**8 段**)。 - **删掉的三段与它们的落点**:`target` 通配只在 `docker_ps` / `docker_attention` → 两条工具自己的 `description` 里已经写着;排障链(attention → ps → logs → grep → inspect)→ `docker_attention`「排障入口」/ `docker_ps`「排障第一步」/ `docker_logs`「改用 `docker_logs_grep`」; 「网络/卷的增删只在面板」→ 本来就没有对应工具。**不是搬到工具描述**(描述也是每轮都付,搬过去 等于没省),是**删重复**。 - **判据**:250 字符(= 预算,**顶格**);`exec-terminal-scope` 的「一次性 / 不受…门控」仍在; 零目标出路仍在且仍在通配之前(顺序判据随通配句一起作废);**新增**一条反查——被删的事实必须 仍在工具描述里(配反例:把 `docker_logs` 描述里的 `docker_logs_grep` 抠掉 → 红)。 - **边界**:顶格意味着下一次改这条公告必须**换掉**同等字符,不能再加。 ### 2026-10-25:`pnpm disclosure-report`(披露面单一出口) - **落点**:`scripts/announcement-budget.mjs`(把测量从 `scripts/test/announcement-budget.test.ts` 抽成纯 JS;测试改为 import 它,fixture/反例留在测试里)、`scripts/disclosure-report.mjs`、 根 `package.json` 的 `disclosure-report`、`scripts/verify-list.mjs`(通用脚本第 4 个)、 `docs/conventions.md` 的对应 bullet。 - **输出四节**:公告长度表(逐条 / 预算 / 余量 / 合计,超预算退出码 1)、MCP 工具面(只读 HTTP, 宿主不可达打 `SKIP` 而不是失败)、档位分类表规模(从各包 `lib` 现算,并注明**不是**各档真实 注册面——那要给真宿主,口径是 `tier-smoke`)、结论 + 相关入口。`--json` 四键。 - **门槛**:`scripts/test/disclosure-report.test.ts` 12 条(表格内容、合计=逐条之和、超预算标出、 弃权/缺失不计入、MCP 可达/不可达两态、`--json` 结构、**两个模块不落盘**的源码级断言)。 - **第一次真跑**:8 段公告合计 **1181** 字符 / 0 段超预算;MCP **69** 个工具;三张分类表合计 **42** 条(read 28 / write 9 / exec 5)。 ### 2026-10-25:公告模板化(原 T7)——**明确不做**,换「总和对」 形态趋同(「本机装有 X;用户提到 Y 时引导打开卡片」)是表象,长度差来自**内容必须不同**: docker 那段是安全契约(D163 守卫钉着)、codegraph 那段是 66 字符的路牌。模板化只会统一便宜的 那部分,然后把带守卫的那句压成模板槽——正好是 D163 的死路。 要动预算结构,改「**逐条 250 保留作纪律 + 总和对**」更贴成本模型:7 段时代合计 931 → 现在 8 段 合计 1181,而真实的账单是**合计**(每轮都在上下文里)。这条**若要做**,判据是「总和上限 + 逐条 250」,两个数都要能从 `disclosure-report` 现算出来(本轮已具备)。 ### 2026-11-04:OS 级同意收尾(PR1–PR3 已落地,PR4 明确不做) - **落点**:[docs/os-consent-plan.md](./docs/os-consent-plan.md)(状态行改为「PR1–PR3 已落地、 PR4 刻意未做」)、`scripts/test/os-consent-scope.test.ts`(§2.1 三条事实 + 基准版本的升级绊线, 24 条用例)、[docs/capability-elevation-plan.md](./docs/capability-elevation-plan.md) §9 的约束指针 (2026-10-07 写入,PR1)。 - **门槛**:宿主升级后 `os-consent-scope` **按设计红**(基准 `0.2.1-rc.5` 对不上 → 必须回来复核三条 事实);「原生框替粘贴」(PR4)**不做也成立**——它把「去宿主终端粘贴一条 `touch`」换成「点一下」, 是 UX 改进、不是安全等级提升:对手 (b)(跨站页面 / 页内脚本)今天已被「写不了宿主文件」挡住 (方案 §0.2)。 - **边界(不做,别当待办重报)**:不碰 tier gate(对手 (d) 归 `permission-tier-plan`)、不碰对手 (c) (同用户进程,出模型)、不把对话框接到 `approval/request` 瀑布(形态 A 会静默丢掉对 (b) 的防护)、 不给 `CapabilityGrantVia` 加 `dialog` 枚举值。 ## 已由 L0 资产承接(不再是待办) | 曾经的形态 | 现在的归属 | |---|---| | 「客户端半体不许从 `location` 拼地址」散在各包注释里 | 已固化为仓库级静态规则 `scripts/client-host-url.mjs`(覆盖全部客户端半体)+ [conventions.md § 客户端半体](./docs/conventions.md#客户端半体六条硬规矩) | | 跨包 DSH_HOME 推导各自一份 | 全仓改走 `@hyzyn/dsh-kit` 的 `dshHome()`,`scripts/check-dsh-home.mjs` 守卫(**codegraph CG35**) | | bundle 补丁重复挂载 | [troubleshooting.md](./docs/troubleshooting.md#安装与挂载) 记症状与成因 | | CI 产物闸门对 `lib/` 恒绿(pathspec `'packages/*/lib'` 命中 0 个文件) | **2026-09-25 已修**:`.github/workflows/ci.yml` 换成 `':(glob)packages/*/lib/**'`,并在该 step 注释里记下这个坑。闸门细节与实测命中数见 [docs/conventions.md § 真机脚本与 CI 接线](./docs/conventions.md#真机脚本与-ci-接线) | | 文档链接闸门只能手动跑 | **2026-09-25 已接线**:`scripts/check-doc-links.mjs` 进 CI(ubuntu-only step),白名单 3 条跨仓相对链接 | | `docs/capability-elevation-plan.md` 的 **v1**(它设计的「宿主终端确认码」通道在代码里不存在) | **2026-09-27 删除**(改名后的 v2 占了这个文件名):内容在 `79ca434e` 已入库,`git show 79ca434e:docs/capability-elevation-plan.md` 取回(223 行;`grep -c terminal-code` = 9,即那份文档的特征段落还在) |