dsh-lan-peers 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DESIGN.md CHANGED
@@ -148,3 +148,52 @@ X-Peer-Sign: base64url(HMAC-SHA256(secret, canonical))
148
148
  - 响应:{enc:'v1', data:'<base64url>'},同一方向密钥加密
149
149
  - requireEncryption 默认 true:缺 X-Peer-Enc 的认证请求 → 400 encryption-required
150
150
  - 未加密:/v1/health(探活)与 /v1/pair(尚无共享密钥,只应在可信局域网配对)
151
+
152
+ ## 12. v0.4.0(在途,未发布)
153
+
154
+ > 状态:实现中。本节是 v0.4.0 的权威契约;发布后本节升为正式条款,同时把 §4.1 的「/v1/pair 明文例外」改为「仅 legacy 兼容路径,默认路径为 ECDH」。
155
+
156
+ 改动两条:**配对不再明文传密钥**(ECDH P-256 两轮)+**派单不再固定 1.5s 轮询**(有界长轮询 + SSE)。总原则:新老双路径,任一端先升级都不破坏互通。
157
+
158
+ ### 12.1 兼容矩阵
159
+
160
+ | 组合 | 要求 |
161
+ |---|---|
162
+ | 旧 0.3.1 客户端 → 新服务端 | `POST /v1/pair` 无 `ecdh` 字段时明文路径必须成功,服务端日志打 `pair: legacy plaintext path used by <peerId>` |
163
+ | 新客户端 → 旧 0.3.1 服务端 | `health.capability` 不含 `pair-ecdh` → 自动回退明文配对 |
164
+ | 旧客户端 → `/v1/answer` | 不带 `waitMs` → 立即返回,语义与 0.3.1 一致 |
165
+ | 新客户端 → 旧服务端 | `capability` 不含 `stream` → 回退 1.5s 轮询 |
166
+
167
+ ### 12.2 端点
168
+
169
+ `GET /v1/health`:capability 追加 `"pair-ecdh"`、`"stream"`,旧值 `"ask"`、`"pair"` 保留。
170
+
171
+ `POST /v1/pair`(明文,第一轮,**不消费配对码**)
172
+ body `{code, peerId, peerName, ecdh:{v:1, curve:"P-256", publicKey:"<base64url, 65 字节未压缩点>"}}`
173
+ → `200 {ecdh:{v:1, curve:"P-256", publicKey}, pairingTtlMs:120000}`。服务端在内存保存 pending(服务端临时私钥 + peerId + expiresAt,120s TTL,不落盘)。无 `ecdh` 字段 → §3 旧路径,行为不变。码不可用 → 401 `invalid-code`;公钥非法 → 400 `bad-request`。
174
+
175
+ `POST /v1/pair/confirm`(明文,消费配对码)
176
+ body `{code, peerId, peerName, wrapped:"<base64url(nonce12‖ct‖tag)>"}`
177
+ 派生 `Z = ECDH(serverPriv, clientPub)`(32B X 坐标),`W = HKDF-SHA256(ikm=Z, salt="dsh-lan-peers/pair/v1", info="dsh-lan-peers/pair/wrap/v1", len=32)`。
178
+ 解密得到客户端发来的 S → 写 peer(`inboundSecret=S`)→ 生成 T,用同一把 W 包裹返回 `200 {peerId, peerName, wrapped:<AES-256-GCM(W, {"secret":T})>}`(nonce 每次新随机)。
179
+ 解密失败 400 `bad-encryption`;无 pending/过期 401 `pairing-expired`;码错 401 `invalid-code`。
180
+
181
+ `GET /v1/answer?sessionId=<id>[&waitMs=<ms>]`(签名+加密)
182
+ 无 `waitMs` → 立即返回(0.3.1 行为);有 `waitMs` → 服务端 hold 到终态或 `min(waitMs, 25000)`,内部每 150ms 调 `runner.poll`。响应 `{status, reply?, error?, elapsedMs}`。
183
+
184
+ `GET /v1/events?sessionId=<id>`(签名,`Accept: text/event-stream`)
185
+ canonical 的 body 用 `sha256("")`。**每条事件载荷独立加密**(与 /v1/answer 同算法、逐条新 nonce),data 形如 `{"enc":"v1","data":"…"}`:
186
+ `status` → `{status:"running", elapsedMs}`;`done` → `{status:"done", reply, elapsedMs}`;`error` → `{status:"error", error, elapsedMs}`;`ping` → 每 10s `{t}`。
187
+ 终态后服务端主动断流;客户端非 200 或中途断流 → 回退长轮询续跑,不重放事件。
188
+
189
+ ### 12.3 配置新增(均有默认值,不填不改变旧行为)
190
+
191
+ `pairingMode: "auto" | "ecdh" | "legacy"`(默认 `auto`,按 capability 探测)、`answerWaitMs: number`(默认 25000,上限 25000)。
192
+
193
+ ### 12.4 模块接口(服务端与客户端共用)
194
+
195
+ `lib/pairing-ecdh.mjs`:`generateKeyPair()` → `{privateKey, publicKeyB64url}`;`deriveWrapKey(privateKey, peerPublicKeyB64url)` → 32B `Buffer`;`wrapSecret(wrapKey, plainObject)` → `base64url(nonce‖ct‖tag)`;`unwrapSecret(wrapKey, wrappedB64url)` → object(失败 throw)。`lib/wire-crypto.mjs` 既有导出不变,只允许新增。
196
+
197
+ ### 12.5 验收
198
+
199
+ 原 §11 六条继续有效,新增:ECDH 两轮成功/被篡改 400/过期 401/码一次性;legacy 路径仍 200 且有弃用告警;`waitMs` 终态提前返回且带 `elapsedMs`、缺省立即返回;/v1/events 每条可解密且终态断流;新旧四类组合互通(见 §12.1)。
package/HANDOVER.md ADDED
@@ -0,0 +1,245 @@
1
+ # dsh-lan-peers 交接文档
2
+
3
+ > 面向接手开发/运维的人。读完这一份应该能:改代码、跑测试、发版、更新部署、排查常见故障。
4
+ > 最后更新:2026-10-27 · 对应版本 `0.4.0`(ECDH 配对 + 流式回传;兼容 0.3.1)
5
+
6
+ ## 1. 这是什么
7
+
8
+ 同一局域网内多个 **DeepSeek Harness (DSH)** 实例的协同插件:一次性配对码互认 → 双向 HMAC 签名 + AES-256-GCM 加密 → agent 之间互相派单并取回结果。
9
+
10
+ **当前状态(已上线)**
11
+
12
+ | 位置 | 形态 | 版本 |
13
+ |---|---|---|
14
+ | npm | 公开包 `dsh-lan-peers`,`latest` | 0.3.1 |
15
+ | NAS `192.168.5.51`(fn-302,Debian 12) | `link:` 安装进 `/home/ai/.dsh/profiles/web`,systemd `dsh-web` 托管 | 0.3.1 |
16
+ | Windows 桌面版(Electron) | 手工装进 `~/.dsh/profiles/desktop`(见 §7) | 0.3.1 |
17
+
18
+ NAS 与桌面已互相配对,双向派单与加密都实测通过。
19
+
20
+ ## 2. 代码结构
21
+
22
+ ```
23
+ lan-peers/
24
+ ├── package.json 元数据;dsh.bundle.patch / dsh.client / exports["./client"]
25
+ ├── cordis.patch.yml bundle 默认行(listenPort 7788,默认拒绝远端派单)
26
+ ├── host/
27
+ │ ├── server.mjs 1610 行 · 协议服务端:签名、加密、配对/ECDH、ask/answer/events、CIDR 白名单
28
+ │ └── test/ auth/api/encryption/pairing-ecdh/stream 测试 + helpers(55 条)
29
+ ├── lib/
30
+ │ ├── index.js 181 行 · DSH 插件入口:解析服务、起 server、注册工具/路由/注入
31
+ │ ├── client.js 205 行 · 桌面/网页客户端组件(__ModuleLoader__ + slots + 派单实时视图)
32
+ │ ├── admin.mjs 388 行 · /api/lan-peers/* 管理接口 + 派单台账 + 网页浮层脚本
33
+ │ ├── tools.mjs 207 行 · lan_peers / lan_peer_ask(v0.4.0 有界长轮询 + 空转保护)
34
+ │ ├── runner-ctx.mjs 55 行 · 进程内 runner(ctx.sessionController,**桌面版必需**)
35
+ │ ├── runner-dsh.mjs 112 行 · HTTP runner(备用,需要本机 token)
36
+ │ ├── wire-crypto.mjs 51 行 · HKDF + AES-256-GCM
37
+ │ ├── pairing-ecdh.mjs 139 行 · v0.4.0 · P-256 两轮配对密钥:generateKeyPair / deriveWrapKey / wrapSecret / unwrapSecret
38
+ │ └── dsh-tools.mjs ~30 行 · defineTool 的稳健解析(CLI / Electron asar / cwd)
39
+ ├── client/
40
+ │ ├── cli.mjs 351 行 · code / pair / peers / ask / events(也是 npm bin)
41
+ │ ├── peer-call.mjs 373 行 · callPeer(签名+加密)/ probePeer / awaitPeerAnswer(SSE→长轮询→1.5s 三级回退)/ pairPeer(ECDH 两轮)
42
+ │ ├── sse.mjs 195 行 · v0.4.0 · fetch+ReadableStream 手写 SSE 解析 + 逐事件解密(EventSource 带不了签名头)
43
+ │ ├── protocol.mjs 60 行 · canonical / HMAC / 防重放 / authHeaders(v0.4.0 新增,供 SSE 带签名头)
44
+ ├── overlays/ web.patch.yml · desktop.patch.yml(部署用覆盖片段)
45
+ ├── README.md 面向使用者的安装文档
46
+ ├── DESIGN.md 协议契约(签名、加密、端点、安全默认、验收用例)
47
+ └── PUBLISH.md 发版步骤
48
+ ```
49
+
50
+ 约定:`host/` 可以脱离 DSH 独立运行(`node host/server.mjs`),`lib/` 才依赖 DSH 的 ctx。
51
+
52
+ ## 3. 协议速查(详见 DESIGN.md)
53
+
54
+ | 端点 | 认证 | 说明 |
55
+ |---|---|---|
56
+ | `GET /v1/health` | 无 | `{name, instanceId, version, capability, peers}` |
57
+ | `POST /v1/pair` | 配对码 | v0.4.0 默认走 ECDH 第一轮(不消费码,返回临时公钥);body 无 `ecdh` → 0.3.1 明文路径(打弃用告警) |
58
+ | `POST /v1/pair/confirm` | 配对码 | v0.4.0 · 第二轮:解 wrapped 得到 S,成功才消费码,返回同一把 W 包裹的 T |
59
+ | `POST /v1/ask` | 签名+加密 | `{requestId, prompt, timeoutSec}` → `202 {sessionId}` |
60
+ | `GET /v1/answer?sessionId=&waitMs=` | 签名+加密 | 无 `waitMs` → 立即返回(0.3.1 语义);有则 hold 到终态或 ≤25s,始终附 `elapsedMs` |
61
+ | `GET /v1/events?sessionId=` | 签名 | v0.4.0 · SSE 事件流(status/done/error/ping),**每条事件独立加密**,终态断流 |
62
+ | `GET /v1/peers` | 签名+加密 | 对端视角的伙伴列表(不含密钥) |
63
+
64
+ 签名:`HMAC-SHA256(secret, "METHOD\nPATH\nTS\nNONCE\nsha256(明文body)")`,base64url;`±60s` 时钟窗;nonce 120s 去重。
65
+
66
+ 加密:`X-Peer-Enc: v1`;密钥 = `HKDF(secret, salt="dsh-lan-peers/v1", info="…/body-encryption/v1")`;`base64url(nonce‖ct‖tag)`;响应 `{"enc":"v1","data":…}`。
67
+
68
+ 错误码:`400 encryption-required | bad-encryption | bad-request`、`401 bad-signature | clock-skew | replay | unknown-peer | invalid-code`、`403 forbidden-network | disabled`、`404 unknown-session`、`413 payload-too-large`、`502 peer-unreachable`。
69
+
70
+ ## 4. 关键设计决定(改代码前先读)
71
+
72
+ 1. **一个伙伴两条密钥**:`secret`(对端发给我的,用来签名)+ `inboundSecret`(我发给对端的,用来验签)。这样双向互认不需要额外握手。`StateStore.upsertPeer` 必须**合并**而不是覆盖,否则会丢另一方向字段。
73
+ 2. **签名覆盖明文、线上传密文**:接收方先解密再验签。这样 GCM 负责密文完整性,签名负责身份绑定,且不需要把签名塞进密文里。
74
+ 3. **桌面版必须用进程内 runner**:Electron 不暴露 web token,HTTP runner 无法建会话。`ctx.sessionController.create/prompt/list` 需要显式 `inject`,且 `prompt`/`list` 必须传 `AbortSignal`(服务里会 `signal.throwIfAborted()`)。
75
+ 4. **插件必须进 `dsh.profile.bundles`**:只写 `cordis.patch.yml` 的插件行,桌面版不会加载(网页版有时会)。这是最难排查的一个坑。
76
+ 5. **管理接口放在 `/api/lan-peers/*`**:桌面窗口是 `dsh-app://`,相对 fetch 只有 `/api` 才会被 IPC 桥接到 host。接口自身还校验 Host 是回环,局域网访问 403。
77
+ 6. **插件返回值不能是对象**:cordis 把 `apply()` 的返回值当 effect,返回 `{server}` 会报 `Invalid effect`。要清理资源用 `ctx.effect(() => () => cleanup)`。
78
+ 7. **热重载需要 disposer**:不注册清理会导致 `EADDRINUSE` + `duplicate route`。
79
+ 8. **undici 对 GET/HEAD 硬拒绝任何 body(含空 Buffer)**:`fetch(url,{method:'GET',body:Buffer.alloc(0)})` 直接抛 `Request with GET/HEAD method cannot have body`。`callPeer` 只能对非 GET/HEAD 传 body(`undefined`,`null` 同样被拒)。**注入 `fetchImpl` 的单测抓不到这条**——0.4.0 期间的真实回归就是这么漏过去的,凡涉及 HTTP 方法语义必须有真 fetch 用例(见 §12 缺陷记录)。
80
+ 9. **旧对端要「完全不发 waitMs」,而不是指望旧服务端忽略它**:0.3.1 服务端不认 `waitMs` 会立即回 `running`,客户端若照发就是秒回热循环;`awaitPeerAnswer` 用 `FAST_ANSWER_MS=500ms` 判定「对端没 hold」并退回 1.5s 节奏。
81
+ 10. **面板不直连对端**:浏览器 `EventSource` 无法携带签名头,所以 SSE 只在插件进程内消费(`client/sse.mjs` 手写解析),面板改为 1s 轮询本地 `/api/lan-peers/dispatch` 台账;面板脚本必须能用 `vm.Script` 独立解析,不引入构建步骤。
82
+ 11. **capability 是运行时探测,不是写死**:客户端所有回退决策都读 `/v1/health` 的 `capability`;测试里那个数组只是契约记账(见 §12 决策记录)。
83
+ 12. **配对码只有单一槽位**:`state.json` 同时只能有一个未消费的码,后出的会顶掉前一个——写自动化脚本时不要并行出多个码(互通 harness 早期因此假 FAIL 过)。
84
+
85
+ ## 5. 本地开发
86
+
87
+ ```bash
88
+ cd lan-peers
89
+ node --test host/test/auth.test.mjs host/test/api.test.mjs host/test/encryption.test.mjs \
90
+ client/protocol.test.mjs lib/wire-crypto.test.mjs # 43 条,应全绿(v0.4.0 起其中 1 条 capability 断言随契约升级)
91
+ node --test host/test/*.test.mjs lib/*.test.mjs client/*.test.mjs # v0.4.0 全量:96 条
92
+
93
+ # 脱离 DSH 起一个独立 host(内存 runner,方便调协议)
94
+ node host/server.mjs --port 7799 --state-dir /tmp/lp --allow-remote-prompts --name dev
95
+
96
+ # 出码/配对/派单
97
+ node client/cli.mjs code --state-dir /tmp/lp-cli
98
+ node client/cli.mjs pair --url http://127.0.0.1:7799 --code 123456 --name dev
99
+ node client/cli.mjs ask --peer dev "hi" --state-dir /tmp/lp-cli
100
+
101
+ # v0.4.0:两轮 ECDH 配对 / 订阅事件流 / 强制走旧明文或旧轮询
102
+ node client/cli.mjs pair --url http://127.0.0.1:7799 --code 123456 --name dev # 默认 auto:能 ECDH 就 ECDH
103
+ node client/cli.mjs pair --url http://127.0.0.1:7799 --code 123456 --legacy-pair
104
+ node client/cli.mjs events --peer dev --session <id> --duration 30
105
+ node client/cli.mjs ask --peer dev "hi" --legacy-poll # 强制 1.5s 轮询,验证旧路径
106
+
107
+ # 新旧互通矩阵(真实进程:两个 host + 两侧真 CLI;约 17s,退出码即结论)
108
+ node .tmp/interop/run-interop.mjs
109
+ # 只测基线时:INTEROP_NEW_ROOT=.tmp/baseline-0.3.1 node .tmp/interop/run-interop.mjs → E/F 自动 SKIP
110
+ ```
111
+
112
+ **日志**:插件每次 apply 都往 `~/.dsh/lan-peers.log` 追加一行(`module imported` / `services:` / `listening` / `failed to listen`)。Electron 的 stdout 拿不到,这是唯一可靠的诊断入口。
113
+
114
+ **调试客户端**:桌面/网页的面板在左下角 🔗;网页版还可以直接访问 `http://<host>/api/lan-peers/panel.js` 看注入的脚本是否有效。
115
+
116
+ ## 6. 发版
117
+
118
+ 三处版本号必须同步:
119
+
120
+ 1. `package.json` 的 `version`
121
+ 2. `host/server.mjs` 的 `PLUGIN_VERSION`(体现在 `/v1/health`)
122
+ 3. (可选)`DESIGN.md` / `README.md` 里提到的版本
123
+
124
+ ```powershell
125
+ cd D:\DS-Harness\lan-peers
126
+ npm publish # 需要登录;见下
127
+ ```
128
+
129
+ **两个已踩过的坑**:
130
+
131
+ - **2FA 暂存发布**:账号开了 2FA 时 `npm publish` 会返回 0 但只创建 `0.0.0-stage` 占位,必须去 npmjs.com 的包页面点 Approve。想跳过就用 Classic **Automation** token。
132
+ - **`bin` 路径**:必须是 `client/cli.mjs`(不能带 `./`),否则 npm 会静默剔除 bin,`npx` 不可用。
133
+
134
+ 发完验证:
135
+
136
+ ```powershell
137
+ npm view dsh-lan-peers version
138
+ # 再装到临时 profile 里跑一次(见 §8 回归脚本)
139
+ ```
140
+
141
+ ## 7. 部署 / 更新
142
+
143
+ ### NAS(link 安装)
144
+
145
+ ```bash
146
+ # 本机打包 → 上传 → 解包 → 重启
147
+ tar czf /tmp/lp.tgz -C D:/DS-Harness/lan-peers lib host client overlays package.json cordis.patch.yml README.md DESIGN.md LICENSE
148
+ scp /tmp/lp.tgz ai@192.168.5.51:/tmp/ # 或用项目里的 fnos-ssh 帮助脚本
149
+ ssh ai@192.168.5.51 "cd /vol1/1000/DSH/lan-peers && tar xzf /tmp/lp.tgz"
150
+ ssh ai@192.168.5.51 "sudo systemctl restart dsh-web"
151
+ ```
152
+
153
+ > 注意:NAS 的 `~` 是 `/home/ai`,profile 在 `/home/ai/.dsh/profiles/web`;插件目录是 `/vol1/1000/DSH/lan-peers`(`link:` 指向它)。
154
+
155
+ ### Windows 桌面版(Electron 独占 profile,CLI 装不进去)
156
+
157
+ 1. 包放到位:`~/.dsh/profiles/desktop/node_modules/dsh-lan-peers`(`pnpm add link:…` 或解压 tarball);
158
+ 2. `~/.dsh/profiles/desktop/package.json`:`dependencies` 加 `"dsh-lan-peers": "link:…"`,并把 `dsh-lan-peers` 加进 `dsh.profile.bundles`;
159
+ 3. `~/.dsh/profiles/desktop/cordis.patch.yml` 贴 `overlays/desktop.patch.yml`;
160
+ 4. 重启桌面版。
161
+
162
+ 改完文件不必重启:**改 profile 的 `cordis.patch.yml` 会触发热重载**(改包内文件不会,需要动一下 profile 文件)。
163
+
164
+ ## 8. 回归检查清单(发版/部署后跑)
165
+
166
+ ```powershell
167
+ # 1) 两端 health
168
+ curl http://127.0.0.1:7788/v1/health
169
+ ssh ai@192.168.5.51 "curl -s http://127.0.0.1:7788/v1/health"
170
+
171
+ # 2) 加密派单(从一个方向即可,双向更好)
172
+ node lan-peers/client/cli.mjs ask --peer <对端名> "回归测试:只回复四个字" --timeout 200 --state-dir <stateDir>
173
+
174
+ # 3) 明文应被拒
175
+ # 用 .tmp/plain-probe.mjs 一类的脚本发一次不带 X-Peer-Enc 的签名请求 → 400 encryption-required
176
+
177
+ 4) 新旧互通矩阵(v0.4.0 起必跑)
178
+ node .tmp/interop/run-interop.mjs # A 旧→新明文+弃用告警 / B 新→旧回退明文 / C 无 waitMs 语义 / D 回退1.5s轮询 / E ECDH 两轮 / F SSE
179
+
180
+ 5) 从 npm 真装
181
+ # 临时 profile:dependencies: {dsh-lan-peers: "^0.4.0"} + bundles 加上它 → 启动 → 看 7788
182
+ ```
183
+
184
+ ## 9. 已知问题 / 技术债
185
+
186
+ | 项 | 说明 | 建议 |
187
+ |---|---|---|
188
+ | ~~配对仍是明文~~ | **v0.4.0 已还清**:默认 ECDH(P-256) 两轮包裹,明文仅作 legacy 兼容路径并打告警 | 发布后删除 legacy 路径(建议 0.5) |
189
+ | 测试 helper 默认 `requireEncryption:false` | 为了保留签名语义用例;加密由 `encryption.test.mjs` 覆盖 | 保持现状,改协议时两边都要跑 |
190
+ | 无 mDNS 自动发现 | 只能手工填地址 | 可加 UDP 广播,但要处理同名/伪造 |
191
+ | ~~派单是轮询、无流式~~ | **v0.4.0 已还清**:`/v1/answer?waitMs` 有界长轮询 + `/v1/events` SSE(新对端自动 SSE,旧对端回落 1.5s 轮询) | 逐字 token 流仍缺(runner 无增量源),需先接 DSH 会话事件 |
192
+ | 单层拓扑 | 无多跳/转发 | 暂不需要 |
193
+ | 长轮询内部 150ms 轮询 | `waitMs` 长轮询期间每 `ANSWER_POLL_INTERVAL_MS=150` 调一次 `runner.poll`,ctx runner 的 poll 内部是 `sessionController.list()`(列全部会话);一次 25s 长轮询约 166 次 | 两实例量级无碍;多实例同机部署时把间隔做成配置项 |
194
+ | `0.0.0-stage` 占位 | npm 暂存发布留下的 | 包页面可删,不影响安装 |
195
+ | Windows 下文件模式 | 测试已跳过 `0600` 断言(POSIX 位不落盘) | 保持 |
196
+
197
+ ## 10. 后续可做
198
+
199
+ 1. ~~配对走 ECDH~~ ~~派单流式回传~~:v0.4.0 已还清;遗留——发布后(建议 0.5)删除 legacy 明文配对路径,并在删除前一版统计其使用次数;
200
+ 2. 客户端卡片正式化
201
+ 2. 配对走 ECDH,彻底消除明文密钥传输;
202
+ 3. 派单流式回传 + 取消(`session/cancel` 已可用);
203
+ 4. 伙伴在线状态与最近活动时间的 UI 展示(`/v1/peers` 已返回 `lastSeen`);
204
+ 5. 多实例同机部署时自动分配端口(当前需手工改 `listenPort`)。
205
+
206
+ ## 11. 交接清单
207
+
208
+ - **npm 账号**:`hanson_sun`(包 maintainer)。发布凭据保存在本机 `~/.npmrc`(token),建议定期轮换。
209
+ - **代码位置**:`D:\DS-Harness\lan-peers`(唯一权威副本;NAS 与桌面都是 `link:` 指向它或同步副本)。
210
+ - **NAS 凭据**:SSH 密钥在 `D:\DS-Harness\ssh-agent-auth\ai_ed25519`,sudo 密码用 DPAPI 加密存在 `~/.dsh/secrets/`(脚本 `~/.dsh/scripts/fnos-secret.ps1`)。
211
+ - **配置位置**:NAS `/home/ai/.dsh/profiles/web/{package.json,cordis.patch.yml}`;Windows `C:\Users\user\.dsh\profiles\desktop\{package.json,cordis.patch.yml}`。
212
+ - **状态/密钥**:各实例 `stateDir` 下的 `state.json`、`peers.json`(0600),含 instanceId 与伙伴密钥,**不要提交或外传**。
213
+
214
+ ## 12. 变更台账(v0.4.0 在途,未发布)
215
+
216
+ > 维护约定:本表随每次落地即时更新,不攒到交付。契约细节见 `DESIGN.md` §12(权威来源)。0.3.1 基线快照冻结在 `D:\DS-Harness\.tmp\baseline-0.3.1`,专供新-旧互通测试。
217
+
218
+ **这一轮要还的债**:§9 表里真正可动的两项 —— 配对明文传密钥、派单 1.5s 轮询无流式。其余条目(测试 helper 默认值、单层拓扑、`0.0.0-stage`、Windows 文件模式)按 §9 的判定保持不动;**mDNS 自动发现本轮明确不做**(要另设计同名冲突与伪造包防护)。
219
+
220
+ | 项 | 归属 | 状态 | 落地后需同步的位置 |
221
+ |---|---|---|---|
222
+ | ECDH(P-256) 两轮配对:`/v1/pair`+`/v1/pair/confirm` | host 道 | **已落地并验收**(43/43、55/55 实测;安全语义已直读核对) | §1 状态表、§3 端点表(已改)、§4 |
223
+ | `/v1/answer?waitMs` 有界长轮询 | host 道 | **已落地并验收** | §3 端点表(已改)、DESIGN §12.2 |
224
+ | `/v1/events` SSE(逐事件加密、fetch 流式读) | host 道 + 客户端道 | **已落地并验收** | §2 结构(已改)、§3(已改)、§4(已改) |
225
+ | `lib/pairing-ecdh.mjs` 共享加密助手 | host 道 | **已落地并验收**(139 行,冻结 4 函数 + 附加导出) | §2 结构(已改)、DESIGN §12.4 |
226
+ | `lan_peer_ask` 长轮询化 + 面板实时状态 + CLI 两轮配对/`events` | 客户端道 | **已落地并验收**(96/96 全量绿;真进程新↔新走 SSE、新→旧走 1.5s 轮询) | §2 结构、§5 命令 |
227
+ | 新-旧互通 harness(真实进程) | 互通道 | **已落地并验收**(`.tmp/interop/`,六行全 PASS,我独立复跑 exit 0;degraded 路径 A–D PASS + E/F SKIP 自证) | §8 回归清单(已改) |
228
+ | capability 追加 `pair-ecdh`/`stream`;legacy 路径保留 + 弃用告警 | host 道 | 实现中 | §3、§9(配对明文行改为"仅 legacy") |
229
+
230
+ **缺陷记录(v0.4.0 期间,均为真进程实测发现)**
231
+
232
+ | 缺陷 | 发现方式 | 根因 | 状态 |
233
+ |---|---|---|---|
234
+ | **已修复并验收**:改为 `body: … ? wire : undefined`,由 harness B/D 两行转 PASS 佐证 |
235
+
236
+ > 教训,写进 §5 本地开发:**注入 `fetchImpl` 的单测抓不到 undici 的请求构造限制**。凡涉及 HTTP 方法语义的断言,必须至少有一条走真实全局 `fetch` + `node:http` 本地服务。
237
+
238
+ **决策记录(v0.4.0 期间)**
239
+
240
+ - **轮询阶段网络错误的语义变化(B 提出,已接受)**:断网 / 对端 502 / 进程被杀,过去由 `callPeer` 抛异常导致工具整体报错;v0.4.0 起 `awaitPeerAnswer` 捕获后返回 `{status:'error'}`(合 DESIGN §6「超时返回 status 而非抛异常中断回合」的先例),`POST /v1/ask` 阶段仍按 0.3.1 原样抛。
241
+ - 旧对端一律「不发 waitMs」,并保留 `FAST_ANSWER_MS=500ms` 秒回兜底判定。
242
+ - `peers.json` 每条 peer 多存 `transport: 'ecdh'|'legacy'`,`upsertPeer` 合并写入,旧读取方无感。
243
+ - `host/test/auth.test.mjs` 的 capability 断言随契约升级
244
+
245
+ **发布时必做**(本轮未开始):`package.json` version 与 `host/server.mjs` 的 `PLUGIN_VERSION` 同步到 0.4.0;NAS 与桌面升级;npm publish(2FA 可能拦成 `0.0.0-stage`,需网页 Approve)。